ref: data structures for plist files

This commit is contained in:
relikd
2025-11-30 15:36:34 +01:00
parent be65aaa19a
commit 72e395c5da
16 changed files with 430 additions and 292 deletions

View File

@@ -11,4 +11,4 @@ MACOSX_DEPLOYMENT_TARGET = 10.15
MARKETING_VERSION = 1.4.0 MARKETING_VERSION = 1.4.0
PRODUCT_NAME = QLAppBundle PRODUCT_NAME = QLAppBundle
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLAppBundle PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLAppBundle
CURRENT_PROJECT_VERSION = 1930 CURRENT_PROJECT_VERSION = 1981

View File

@@ -9,8 +9,8 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF5D2EA1199B00613856 /* MetaInfo.swift */; }; 5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF5D2EA1199B00613856 /* MetaInfo.swift */; };
5405CF652EA1376B00613856 /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF642EA1376B00613856 /* Zip.swift */; }; 5405CF652EA1376B00613856 /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF642EA1376B00613856 /* Zip.swift */; };
540B77D92ED79BBD009E030C /* MetaInfo+Apk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540B77D82ED79BB2009E030C /* MetaInfo+Apk.swift */; }; 540B77D92ED79BBD009E030C /* ApkManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540B77D82ED79BB2009E030C /* ApkManifest.swift */; };
540B77DA2ED79C6B009E030C /* MetaInfo+Apk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540B77D82ED79BB2009E030C /* MetaInfo+Apk.swift */; }; 540B77DA2ED79C6B009E030C /* ApkManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540B77D82ED79BB2009E030C /* ApkManifest.swift */; };
540B77DC2ED79CC1009E030C /* AndroidXML in Frameworks */ = {isa = PBXBuildFile; productRef = 540B77DB2ED79CC1009E030C /* AndroidXML */; }; 540B77DC2ED79CC1009E030C /* AndroidXML in Frameworks */ = {isa = PBXBuildFile; productRef = 540B77DB2ED79CC1009E030C /* AndroidXML */; };
540B77DE2ED79CC8009E030C /* AndroidXML in Frameworks */ = {isa = PBXBuildFile; productRef = 540B77DD2ED79CC8009E030C /* AndroidXML */; }; 540B77DE2ED79CC8009E030C /* AndroidXML in Frameworks */ = {isa = PBXBuildFile; productRef = 540B77DD2ED79CC8009E030C /* AndroidXML */; };
5412DECE2EBC168600F9040D /* Preview+ArchiveInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412DECD2EBC168600F9040D /* Preview+ArchiveInfo.swift */; }; 5412DECE2EBC168600F9040D /* Preview+ArchiveInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412DECD2EBC168600F9040D /* Preview+ArchiveInfo.swift */; };
@@ -41,6 +41,12 @@
547F52F42EB2CA05002B6D5F /* Preview+Entitlements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547F52F32EB2CA05002B6D5F /* Preview+Entitlements.swift */; }; 547F52F42EB2CA05002B6D5F /* Preview+Entitlements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547F52F32EB2CA05002B6D5F /* Preview+Entitlements.swift */; };
547F52F72EB2CAC7002B6D5F /* Preview+Footer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547F52F62EB2CAC7002B6D5F /* Preview+Footer.swift */; }; 547F52F72EB2CAC7002B6D5F /* Preview+Footer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547F52F62EB2CAC7002B6D5F /* Preview+Footer.swift */; };
547F52F92EB2CBAB002B6D5F /* Date+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547F52F82EB2CBAB002B6D5F /* Date+Format.swift */; }; 547F52F92EB2CBAB002B6D5F /* Date+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547F52F82EB2CBAB002B6D5F /* Date+Format.swift */; };
54993B882EDB9AA1008B656D /* Plist+Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54993B872EDB9A9B008B656D /* Plist+Info.swift */; };
54993B8A2EDBA596008B656D /* Plist+Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54993B892EDBA596008B656D /* Plist+Icon.swift */; };
54993B8B2EDBA75A008B656D /* Plist+Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54993B892EDBA596008B656D /* Plist+Icon.swift */; };
54993B932EDBB41D008B656D /* Plist+iTunesMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54993B922EDBB419008B656D /* Plist+iTunesMetadata.swift */; };
54993B952EDBC819008B656D /* Plist+MobileProvision.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54993B942EDBC813008B656D /* Plist+MobileProvision.swift */; };
54993B972EDC7C65008B656D /* Provisioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54993B962EDC7C61008B656D /* Provisioning.swift */; };
549E3B9F2EBA9D2500ADFF56 /* QLAppBundle Preview Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 54442C202E378BAF008A870E /* QLAppBundle Preview Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 549E3B9F2EBA9D2500ADFF56 /* QLAppBundle Preview Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 54442C202E378BAF008A870E /* QLAppBundle Preview Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
549E3BA12EBAE7D300ADFF56 /* URL+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E3BA02EBAE7D300ADFF56 /* URL+File.swift */; }; 549E3BA12EBAE7D300ADFF56 /* URL+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E3BA02EBAE7D300ADFF56 /* URL+File.swift */; };
549E3BA22EBAECD400ADFF56 /* URL+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E3BA02EBAE7D300ADFF56 /* URL+File.swift */; }; 549E3BA22EBAECD400ADFF56 /* URL+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E3BA02EBAE7D300ADFF56 /* URL+File.swift */; };
@@ -130,7 +136,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
5405CF5D2EA1199B00613856 /* MetaInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaInfo.swift; sourceTree = "<group>"; }; 5405CF5D2EA1199B00613856 /* MetaInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaInfo.swift; sourceTree = "<group>"; };
5405CF642EA1376B00613856 /* Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zip.swift; sourceTree = "<group>"; }; 5405CF642EA1376B00613856 /* Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zip.swift; sourceTree = "<group>"; };
540B77D82ED79BB2009E030C /* MetaInfo+Apk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MetaInfo+Apk.swift"; sourceTree = "<group>"; }; 540B77D82ED79BB2009E030C /* ApkManifest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApkManifest.swift; sourceTree = "<group>"; };
5412DECD2EBC168600F9040D /* Preview+ArchiveInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preview+ArchiveInfo.swift"; sourceTree = "<group>"; }; 5412DECD2EBC168600F9040D /* Preview+ArchiveInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preview+ArchiveInfo.swift"; sourceTree = "<group>"; };
5412DECF2EBC283000F9040D /* RuntimeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeError.swift; sourceTree = "<group>"; }; 5412DECF2EBC283000F9040D /* RuntimeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeError.swift; sourceTree = "<group>"; };
54352E8A2EB6A79A0082F61D /* AssetCarReader.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AssetCarReader.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 54352E8A2EB6A79A0082F61D /* AssetCarReader.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AssetCarReader.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -160,6 +166,11 @@
547F52F82EB2CBAB002B6D5F /* Date+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Format.swift"; sourceTree = "<group>"; }; 547F52F82EB2CBAB002B6D5F /* Date+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Format.swift"; sourceTree = "<group>"; };
547F52FB2EB37F10002B6D5F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; }; 547F52FB2EB37F10002B6D5F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
547F52FC2EB37F3A002B6D5F /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; }; 547F52FC2EB37F3A002B6D5F /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
54993B872EDB9A9B008B656D /* Plist+Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Plist+Info.swift"; sourceTree = "<group>"; };
54993B892EDBA596008B656D /* Plist+Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Plist+Icon.swift"; sourceTree = "<group>"; };
54993B922EDBB419008B656D /* Plist+iTunesMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Plist+iTunesMetadata.swift"; sourceTree = "<group>"; };
54993B942EDBC813008B656D /* Plist+MobileProvision.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Plist+MobileProvision.swift"; sourceTree = "<group>"; };
54993B962EDC7C61008B656D /* Provisioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Provisioning.swift; sourceTree = "<group>"; };
549E3B9E2EBA8FDA00ADFF56 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; }; 549E3B9E2EBA8FDA00ADFF56 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
549E3BA02EBAE7D300ADFF56 /* URL+File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+File.swift"; sourceTree = "<group>"; }; 549E3BA02EBAE7D300ADFF56 /* URL+File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+File.swift"; sourceTree = "<group>"; };
549E3BA32EBC021500ADFF56 /* Preview+TransportSecurity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preview+TransportSecurity.swift"; sourceTree = "<group>"; }; 549E3BA32EBC021500ADFF56 /* Preview+TransportSecurity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preview+TransportSecurity.swift"; sourceTree = "<group>"; };
@@ -222,10 +233,15 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5405CF5D2EA1199B00613856 /* MetaInfo.swift */, 5405CF5D2EA1199B00613856 /* MetaInfo.swift */,
540B77D82ED79BB2009E030C /* MetaInfo+Apk.swift */,
5412DECF2EBC283000F9040D /* RuntimeError.swift */, 5412DECF2EBC283000F9040D /* RuntimeError.swift */,
54D3A6EB2EA31B52001EF4F6 /* AppCategories.swift */,
54D3A6ED2EA39CC6001EF4F6 /* AppIcon.swift */, 54D3A6ED2EA39CC6001EF4F6 /* AppIcon.swift */,
54993B892EDBA596008B656D /* Plist+Icon.swift */,
54993B872EDB9A9B008B656D /* Plist+Info.swift */,
54993B922EDBB419008B656D /* Plist+iTunesMetadata.swift */,
54993B942EDBC813008B656D /* Plist+MobileProvision.swift */,
54993B962EDC7C61008B656D /* Provisioning.swift */,
540B77D82ED79BB2009E030C /* ApkManifest.swift */,
54D3A6EB2EA31B52001EF4F6 /* AppCategories.swift */,
5469E11C2EA5930C00D46CE7 /* Entitlements.swift */, 5469E11C2EA5930C00D46CE7 /* Entitlements.swift */,
547F52DC2EB2C15D002B6D5F /* ExpirationStatus.swift */, 547F52DC2EB2C15D002B6D5F /* ExpirationStatus.swift */,
547F52E62EB2C41C002B6D5F /* PreviewGenerator.swift */, 547F52E62EB2C41C002B6D5F /* PreviewGenerator.swift */,
@@ -552,20 +568,25 @@
54D3A6F02EA3F49F001EF4F6 /* NSBezierPath+RoundedRect.swift in Sources */, 54D3A6F02EA3F49F001EF4F6 /* NSBezierPath+RoundedRect.swift in Sources */,
547F52E42EB2C3D8002B6D5F /* Preview+iTunesPurchase.swift in Sources */, 547F52E42EB2C3D8002B6D5F /* Preview+iTunesPurchase.swift in Sources */,
547F52F72EB2CAC7002B6D5F /* Preview+Footer.swift in Sources */, 547F52F72EB2CAC7002B6D5F /* Preview+Footer.swift in Sources */,
54993B8A2EDBA596008B656D /* Plist+Icon.swift in Sources */,
54993B972EDC7C65008B656D /* Provisioning.swift in Sources */,
5469E11D2EA5930C00D46CE7 /* Entitlements.swift in Sources */, 5469E11D2EA5930C00D46CE7 /* Entitlements.swift in Sources */,
54442C792E378BE0008A870E /* PreviewViewController.swift in Sources */, 54442C792E378BE0008A870E /* PreviewViewController.swift in Sources */,
547F52EF2EB2C8E8002B6D5F /* Preview+Provisioning.swift in Sources */, 547F52EF2EB2C8E8002B6D5F /* Preview+Provisioning.swift in Sources */,
547F52DE2EB2C15D002B6D5F /* ExpirationStatus.swift in Sources */, 547F52DE2EB2C15D002B6D5F /* ExpirationStatus.swift in Sources */,
54D3A6EE2EA39CC6001EF4F6 /* AppIcon.swift in Sources */, 54D3A6EE2EA39CC6001EF4F6 /* AppIcon.swift in Sources */,
54993B952EDBC819008B656D /* Plist+MobileProvision.swift in Sources */,
547F52E82EB2C41C002B6D5F /* PreviewGenerator.swift in Sources */, 547F52E82EB2C41C002B6D5F /* PreviewGenerator.swift in Sources */,
54993B882EDB9AA1008B656D /* Plist+Info.swift in Sources */,
547F52EB2EB2C672002B6D5F /* Preview+FileInfo.swift in Sources */, 547F52EB2EB2C672002B6D5F /* Preview+FileInfo.swift in Sources */,
547F52ED2EB2C822002B6D5F /* Preview+AppInfo.swift in Sources */, 547F52ED2EB2C822002B6D5F /* Preview+AppInfo.swift in Sources */,
549E3BA12EBAE7D300ADFF56 /* URL+File.swift in Sources */, 549E3BA12EBAE7D300ADFF56 /* URL+File.swift in Sources */,
547F52F42EB2CA05002B6D5F /* Preview+Entitlements.swift in Sources */, 547F52F42EB2CA05002B6D5F /* Preview+Entitlements.swift in Sources */,
540B77D92ED79BBD009E030C /* MetaInfo+Apk.swift in Sources */, 540B77D92ED79BBD009E030C /* ApkManifest.swift in Sources */,
5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */, 5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */,
547F52F92EB2CBAB002B6D5F /* Date+Format.swift in Sources */, 547F52F92EB2CBAB002B6D5F /* Date+Format.swift in Sources */,
54D3A6EC2EA31B52001EF4F6 /* AppCategories.swift in Sources */, 54D3A6EC2EA31B52001EF4F6 /* AppCategories.swift in Sources */,
54993B932EDBB41D008B656D /* Plist+iTunesMetadata.swift in Sources */,
5405CF652EA1376B00613856 /* Zip.swift in Sources */, 5405CF652EA1376B00613856 /* Zip.swift in Sources */,
5412DED02EBC283000F9040D /* RuntimeError.swift in Sources */, 5412DED02EBC283000F9040D /* RuntimeError.swift in Sources */,
); );
@@ -581,7 +602,8 @@
54AE5BFF2EB3DB1000B4CFC7 /* ThumbnailProvider.swift in Sources */, 54AE5BFF2EB3DB1000B4CFC7 /* ThumbnailProvider.swift in Sources */,
549E3BA22EBAECD400ADFF56 /* URL+File.swift in Sources */, 549E3BA22EBAECD400ADFF56 /* URL+File.swift in Sources */,
547899752EB38F3D00F96B80 /* AppIcon.swift in Sources */, 547899752EB38F3D00F96B80 /* AppIcon.swift in Sources */,
540B77DA2ED79C6B009E030C /* MetaInfo+Apk.swift in Sources */, 540B77DA2ED79C6B009E030C /* ApkManifest.swift in Sources */,
54993B8B2EDBA75A008B656D /* Plist+Icon.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -17,7 +17,7 @@ struct AppIcon {
func extractImageForThumbnail() -> NSImage { func extractImageForThumbnail() -> NSImage {
switch meta.type { switch meta.type {
case .IPA, .Archive, .Extension: case .IPA, .Archive, .Extension:
extractImage(from: meta.readPlistApp()) extractImage(from: meta.readPlist_Icon()?.filenames)
case .APK: case .APK:
extractImage(from: meta.readApkIconOnly()) extractImage(from: meta.readApkIconOnly())
} }
@@ -33,7 +33,7 @@ struct AppIcon {
/// Try multiple methods to extract image. /// 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. /// 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 // no need to unwrap the plist, and most .ipa should include the Artwork anyway
if meta.type == .IPA { if meta.type == .IPA {
if let data = meta.zipFile!.unzipFile("iTunesArtwork") { if let data = meta.zipFile!.unzipFile("iTunesArtwork") {
@@ -44,7 +44,7 @@ struct AppIcon {
} }
// Extract image name from app plist // 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) os_log(.debug, log: log, "[icon] icon names in plist: %{public}@", plistImgNames)
// If no previous filename works (or empty), try default icon names // If no previous filename works (or empty), try default icon names
@@ -90,33 +90,6 @@ struct AppIcon {
// MARK: - Plist // MARK: - Plist
extension AppIcon { 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. /// Given a filename, search Bundle or Filesystem for files that match. Select the filename with the highest resolution.
private func expandImageName(_ iconList: [String]) -> String? { private func expandImageName(_ iconList: [String]) -> String? {
var matches: [String] = [] var matches: [String] = []
@@ -162,21 +135,6 @@ extension AppIcon {
return matches.isEmpty ? nil : sortedByResolution(matches).first 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. /// @return lower index means higher resolution.
private func resolutionIndex(_ iconName: String) -> Int { private func resolutionIndex(_ iconName: String) -> Int {
let lower = iconName.lowercased() let lower = iconName.lowercased()

View File

@@ -85,16 +85,6 @@ struct MetaInfo {
return try? Data(contentsOf: self.effectiveUrl(osxSubdir, filename)) 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
View 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
View 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 []
}

View 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)"
}

View 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
}

View File

@@ -1,45 +1,21 @@
import Foundation import Foundation
extension PreviewGenerator { 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` /// Process info stored in `Info.plist`
mutating func procAppInfoApple(_ appPlist: PlistDict, isOSX: Bool) { mutating func procAppInfoApple(_ appPlist: Plist_Info) {
let minVersion = appPlist[isOSX ? "LSMinimumSystemVersion" : "MinimumOSVersion"] as? String ?? ""
let extensionType = (appPlist["NSExtension"] as? PlistDict)?["NSExtensionPointIdentifier"] as? String
self.apply([ self.apply([
"AppInfoHidden": CLASS_VISIBLE, "AppInfoHidden": CLASS_VISIBLE,
"AppName": appPlist["CFBundleDisplayName"] as? String ?? appPlist["CFBundleName"] as? String ?? "", "AppName": appPlist.name ?? "",
"AppVersion": appPlist["CFBundleShortVersionString"] as? String ?? "", "AppVersion": appPlist.version ?? "",
"AppBuildVer": appPlist["CFBundleVersion"] as? String ?? "", "AppBuildVer": appPlist.buildVersion ?? "",
"AppId": appPlist["CFBundleIdentifier"] as? String ?? "", "AppId": appPlist.bundleId ?? "",
"AppExtensionTypeHidden": extensionType != nil ? CLASS_VISIBLE : CLASS_HIDDEN, "AppExtensionTypeHidden": appPlist.extensionType != nil ? CLASS_VISIBLE : CLASS_HIDDEN,
"AppExtensionType": extensionType ?? "", "AppExtensionType": appPlist.extensionType ?? "",
"AppDeviceFamily": deviceFamilyList(appPlist, isOSX: isOSX), "AppDeviceFamily": appPlist.deviceFamily.joined(separator: ", "),
"AppSDK": appPlist["DTSDKName"] as? String ?? "", "AppSDK": appPlist.sdkVersion ?? "",
"AppMinOS": minVersion, "AppMinOS": appPlist.minOS ?? "",
]) ])
} }

View File

@@ -22,9 +22,9 @@ extension PreviewGenerator {
} }
/// Process compiled binary and provision plist to extract `Entitlements` /// Process compiled binary and provision plist to extract `Entitlements`
mutating func procEntitlements(_ meta: MetaInfo, _ appPlist: PlistDict?, _ provisionPlist: PlistDict?) { mutating func procEntitlements(_ meta: MetaInfo, _ appPlist: Plist_Info?, _ provisionPlist: Plist_MobileProvision?) {
var entitlements = readEntitlements(meta, appPlist?["CFBundleExecutable"] as? String) var entitlements = readEntitlements(meta, appPlist?.exePath)
entitlements.applyFallbackIfNeeded(provisionPlist?["Entitlements"] as? PlistDict) entitlements.applyFallbackIfNeeded(provisionPlist?.entitlements)
if entitlements.html == nil && !entitlements.hasError { if entitlements.html == nil && !entitlements.hasError {
return return

View File

@@ -1,148 +1,57 @@
import Foundation 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 { 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` /// Process info stored in `embedded.mobileprovision`
mutating func procProvision(_ provisionPlist: PlistDict?, isOSX: Bool) { mutating func procProvision(_ provisionPlist: Plist_MobileProvision?) {
guard let provisionPlist else { guard let provisionPlist else {
return return
} }
let creationDate = provisionPlist["CreationDate"] as? Date let deviceCount = provisionPlist.devices.count
let expireDate = provisionPlist["ExpirationDate"] as? Date
let devices = getDeviceList(provisionPlist)
let certs = getCertificateList(provisionPlist)
self.apply([ self.apply([
"ProvisionHidden": CLASS_VISIBLE, "ProvisionHidden": CLASS_VISIBLE,
"ProvisionProfileName": provisionPlist["Name"] as? String ?? "", "ProvisionProfileName": provisionPlist.profileName ?? "",
"ProvisionProfileId": provisionPlist["UUID"] as? String ?? "", "ProvisionProfileId": provisionPlist.profileId ?? "",
"ProvisionTeamName": provisionPlist["TeamName"] as? String ?? "<em>Team name not available</em>", "ProvisionTeamName": provisionPlist.teamName ?? "<em>Team name not available</em>",
"ProvisionTeamIds": (provisionPlist["TeamIdentifier"] as? [String])?.joined(separator: ", ") ?? "<em>Team ID not available</em>", "ProvisionTeamIds": provisionPlist.teamIds.isEmpty ? "<em>Team ID not available</em>" : provisionPlist.teamIds.joined(separator: ", "),
"ProvisionCreateDate": creationDate?.formattedCreationDate() ?? "", "ProvisionCreateDate": provisionPlist.creationDate?.formattedCreationDate() ?? "",
"ProvisionExpireDate": expireDate?.formattedExpirationDate() ?? "", "ProvisionExpireDate": provisionPlist.expireDate?.formattedExpirationDate() ?? "",
"ProvisionExpireStatus": ExpirationStatus(expireDate).cssClass(), "ProvisionExpireStatus": ExpirationStatus(provisionPlist.expireDate).cssClass(),
"ProvisionProfilePlatform": isOSX ? "Mac" : "iOS", "ProvisionProfilePlatform": provisionPlist.profilePlatform,
"ProvisionProfileType": stringForProfileType(provisionPlist, isOSX: isOSX), "ProvisionProfileType": provisionPlist.profileType,
"ProvisionDeviceCount": devices.isEmpty ? "No Devices" : "\(devices.count) Device\(devices.count == 1 ? "" : "s")", "ProvisionDeviceCount": deviceCount == 0 ? "No Devices" : "\(deviceCount) Device\(deviceCount == 1 ? "" : "s")",
"ProvisionDeviceIds": devices.isEmpty ? "Distribution Profile" : formatAsTable(devices, header: ["", "UDID"]), "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] private typealias TableRow = [String]
/// Print html table with arbitrary number of columns /// Print html table with arbitrary number of columns

View File

@@ -43,8 +43,8 @@ private func recursiveTransportSecurity(_ dictionary: PlistDict, _ level: Int =
extension PreviewGenerator { extension PreviewGenerator {
/// Process ATS info in `Info.plist` /// Process ATS info in `Info.plist`
mutating func procTransportSecurity(_ appPlist: PlistDict?) { mutating func procTransportSecurity(_ appPlist: Plist_Info?) {
guard let value = appPlist?["NSAppTransportSecurity"] as? PlistDict else { guard let value = appPlist?.transportSecurity else {
return return
} }

View File

@@ -1,69 +1,21 @@
import Foundation 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 { 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` /// Process info stored in `iTunesMetadata.plist`
mutating func procItunesMeta(_ itunesPlist: PlistDict?) { mutating func procItunesMeta(_ itunesPlist: Plist_iTunesMetadata?) {
guard let itunesPlist else { guard let itunesPlist else {
return 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([ self.apply([
"iTunesHidden": CLASS_VISIBLE, "iTunesHidden": CLASS_VISIBLE,
"iTunesId": (itunesPlist["itemId"] as? Int)?.description ?? "", "iTunesId": itunesPlist.appId?.description ?? "",
"iTunesName": itunesPlist["itemName"] as? String ?? "", "iTunesName": itunesPlist.appName ?? "",
"iTunesGenres": formattedGenres(itunesPlist), "iTunesGenres": itunesPlist.genres.joined(separator: ", "),
"iTunesReleaseDate": releaseDate?.mediumFormat() ?? "", "iTunesReleaseDate": itunesPlist.releaseDate?.mediumFormat() ?? "",
"iTunesAppleId": name, "iTunesAppleId": itunesPlist.purchaserName ?? "",
"iTunesPurchaseDate": purchaseDate?.mediumFormat() ?? "", "iTunesPurchaseDate": itunesPlist.purchaseDate?.mediumFormat() ?? "",
"iTunesPrice": itunesPlist["priceDisplay"] as? String ?? "", "iTunesPrice": itunesPlist.price ?? "",
]) ])
} }
} }

View File

@@ -26,22 +26,22 @@ struct PreviewGenerator {
switch meta.type { switch meta.type {
case .IPA, .Archive, .Extension: case .IPA, .Archive, .Extension:
guard let plistApp = meta.readPlistApp() else { guard let plistApp = meta.readPlist_Info() else {
throw RuntimeError("Info.plist not found") throw RuntimeError("Info.plist not found")
} }
procAppInfoApple(plistApp, isOSX: meta.isOSX) procAppInfoApple(plistApp)
if meta.type == .IPA { if meta.type == .IPA {
procItunesMeta(meta.readPlistItunes()) procItunesMeta(meta.readPlist_iTunesMetadata())
} else if meta.type == .Archive { } else if meta.type == .Archive {
procArchiveInfo(meta.readPlistXCArchive()) procArchiveInfo(meta.readPlistXCArchive())
} }
procTransportSecurity(plistApp) procTransportSecurity(plistApp)
let plistProvision = meta.readPlistProvision() let plistProvision = meta.readPlist_MobileProvision()
procEntitlements(meta, plistApp, plistProvision) procEntitlements(meta, plistApp, plistProvision)
procProvision(plistProvision, isOSX: meta.isOSX) procProvision(plistProvision)
// App Icon (last, because the image uses a lot of memory) // 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: case .APK:
guard let manifest = meta.readApkManifest() else { guard let manifest = meta.readApkManifest() else {

58
src/Provisioning.swift Normal file
View 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)
}