Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be65aaa19a | ||
|
|
feae5aba3e | ||
|
|
3a587ce730 | ||
|
|
6bb62bdf82 | ||
|
|
f233d0e4a2 | ||
|
|
035276dcfc | ||
|
|
3a16277867 | ||
|
|
591a75dabc | ||
|
|
cde957b01f | ||
|
|
71d1b35aac | ||
|
|
5cd7034fc8 |
@@ -27,6 +27,26 @@
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.google.android.apk</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>application/vnd.android.package-archive</string>
|
||||
</array>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>apk</string>
|
||||
<string>apkm</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [1.4.0] – 2025-11-29
|
||||
Added:
|
||||
- Support for `.apk` files
|
||||
- Support for `.apkm` files
|
||||
|
||||
|
||||
## [1.3.0] – 2025-11-06
|
||||
Added:
|
||||
- Show macOS apps in `.xcarchive`
|
||||
@@ -37,6 +43,7 @@ Added:
|
||||
Initial release
|
||||
|
||||
|
||||
[1.4.0]: https://github.com/relikd/QLAppBundle/compare/v1.3.0...v1.4.0
|
||||
[1.3.0]: https://github.com/relikd/QLAppBundle/compare/v1.2.0...v1.3.0
|
||||
[1.2.0]: https://github.com/relikd/QLAppBundle/compare/v1.1.0...v1.2.0
|
||||
[1.1.0]: https://github.com/relikd/QLAppBundle/compare/v1.0.0...v1.1.0
|
||||
|
||||
@@ -8,6 +8,7 @@ ENABLE_HARDENED_RUNTIME = YES
|
||||
SWIFT_VERSION = 5.0
|
||||
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15
|
||||
MARKETING_VERSION = 1.3.0
|
||||
MARKETING_VERSION = 1.4.0
|
||||
PRODUCT_NAME = QLAppBundle
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLAppBundle
|
||||
CURRENT_PROJECT_VERSION = 1930
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF5D2EA1199B00613856 /* MetaInfo.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 */; };
|
||||
540B77DA2ED79C6B009E030C /* MetaInfo+Apk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540B77D82ED79BB2009E030C /* MetaInfo+Apk.swift */; };
|
||||
540B77DC2ED79CC1009E030C /* AndroidXML in Frameworks */ = {isa = PBXBuildFile; productRef = 540B77DB2ED79CC1009E030C /* AndroidXML */; };
|
||||
540B77DE2ED79CC8009E030C /* AndroidXML in Frameworks */ = {isa = PBXBuildFile; productRef = 540B77DD2ED79CC8009E030C /* AndroidXML */; };
|
||||
5412DECE2EBC168600F9040D /* Preview+ArchiveInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412DECD2EBC168600F9040D /* Preview+ArchiveInfo.swift */; };
|
||||
5412DED02EBC283000F9040D /* RuntimeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412DECF2EBC283000F9040D /* RuntimeError.swift */; };
|
||||
543FE5742EB3BB5E0059F98B /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 543FE5732EB3BB5E0059F98B /* AppIcon.icns */; };
|
||||
@@ -126,6 +130,7 @@
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
540B77D82ED79BB2009E030C /* MetaInfo+Apk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MetaInfo+Apk.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>"; };
|
||||
54352E8A2EB6A79A0082F61D /* AssetCarReader.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AssetCarReader.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -192,6 +197,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
540B77DC2ED79CC1009E030C /* AndroidXML in Frameworks */,
|
||||
54B6FFF02EB6AA0F007397C0 /* AssetCarReader.framework in Frameworks */,
|
||||
54D3A6FE2EA465B4001EF4F6 /* CoreGraphics.framework in Frameworks */,
|
||||
54442C232E378BAF008A870E /* Quartz.framework in Frameworks */,
|
||||
@@ -202,6 +208,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
540B77DE2ED79CC8009E030C /* AndroidXML in Frameworks */,
|
||||
54B6FFF52EB6AA14007397C0 /* AssetCarReader.framework in Frameworks */,
|
||||
54581FD12EB29A0B0043A0B3 /* QuickLookThumbnailing.framework in Frameworks */,
|
||||
54581FD22EB29A0B0043A0B3 /* Quartz.framework in Frameworks */,
|
||||
@@ -215,6 +222,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5405CF5D2EA1199B00613856 /* MetaInfo.swift */,
|
||||
540B77D82ED79BB2009E030C /* MetaInfo+Apk.swift */,
|
||||
5412DECF2EBC283000F9040D /* RuntimeError.swift */,
|
||||
54D3A6EB2EA31B52001EF4F6 /* AppCategories.swift */,
|
||||
54D3A6ED2EA39CC6001EF4F6 /* AppIcon.swift */,
|
||||
@@ -406,6 +414,7 @@
|
||||
);
|
||||
name = "QL Preview";
|
||||
packageProductDependencies = (
|
||||
540B77DB2ED79CC1009E030C /* AndroidXML */,
|
||||
);
|
||||
productName = QLPreview;
|
||||
productReference = 54442C202E378BAF008A870E /* QLAppBundle Preview Extension.appex */;
|
||||
@@ -427,6 +436,7 @@
|
||||
);
|
||||
name = "QL Thumbnail";
|
||||
packageProductDependencies = (
|
||||
540B77DD2ED79CC8009E030C /* AndroidXML */,
|
||||
);
|
||||
productName = QLThumbnail;
|
||||
productReference = 54581FCF2EB29A0B0043A0B3 /* QLAppBundle Thumbnail Extension.appex */;
|
||||
@@ -462,6 +472,9 @@
|
||||
);
|
||||
mainGroup = 54442BEB2E378B71008A870E;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
54D891112ED7313100BF23C4 /* XCRemoteSwiftPackageReference "AndroidXML" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 54442BF52E378B71008A870E /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -549,6 +562,7 @@
|
||||
547F52ED2EB2C822002B6D5F /* Preview+AppInfo.swift in Sources */,
|
||||
549E3BA12EBAE7D300ADFF56 /* URL+File.swift in Sources */,
|
||||
547F52F42EB2CA05002B6D5F /* Preview+Entitlements.swift in Sources */,
|
||||
540B77D92ED79BBD009E030C /* MetaInfo+Apk.swift in Sources */,
|
||||
5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */,
|
||||
547F52F92EB2CBAB002B6D5F /* Date+Format.swift in Sources */,
|
||||
54D3A6EC2EA31B52001EF4F6 /* AppCategories.swift in Sources */,
|
||||
@@ -567,6 +581,7 @@
|
||||
54AE5BFF2EB3DB1000B4CFC7 /* ThumbnailProvider.swift in Sources */,
|
||||
549E3BA22EBAECD400ADFF56 /* URL+File.swift in Sources */,
|
||||
547899752EB38F3D00F96B80 /* AppIcon.swift in Sources */,
|
||||
540B77DA2ED79C6B009E030C /* MetaInfo+Apk.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -720,7 +735,6 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1791;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
@@ -787,7 +801,6 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1791;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
@@ -823,6 +836,7 @@
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
@@ -849,6 +863,7 @@
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
@@ -1016,6 +1031,30 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
54D891112ED7313100BF23C4 /* XCRemoteSwiftPackageReference "AndroidXML" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/relikd/AndroidXML";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 0.9.4;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
540B77DB2ED79CC1009E030C /* AndroidXML */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 54D891112ED7313100BF23C4 /* XCRemoteSwiftPackageReference "AndroidXML" */;
|
||||
productName = AndroidXML;
|
||||
};
|
||||
540B77DD2ED79CC8009E030C /* AndroidXML */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 54D891112ED7313100BF23C4 /* XCRemoteSwiftPackageReference "AndroidXML" */;
|
||||
productName = AndroidXML;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 54442BEC2E378B71008A870E /* Project object */;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"originHash" : "c869761611793a3eebb4e2f56e7aebab4faa8db4159e6116b059292c98af7094",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "androidxml",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/relikd/AndroidXML",
|
||||
"state" : {
|
||||
"revision" : "d9fe646bcc3b05548aebbd20b4eee0af675c129f",
|
||||
"version" : "0.9.4"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@@ -15,6 +15,10 @@
|
||||
<string>com.apple.xcode.archive</string>
|
||||
<string>com.opa334.trollstore.tipa</string>
|
||||
<string>dyn.ah62d4rv4ge81k4puqe</string>
|
||||
<string>com.google.android.apk</string>
|
||||
<string>dyn.ah62d4rv4ge80c6dp</string>
|
||||
<string>public.archive.apk</string>
|
||||
<string>dyn.ah62d4rv4ge80c6dpry</string>
|
||||
</array>
|
||||
<key>QLSupportsSearchableItems</key>
|
||||
<false/>
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
<string>com.apple.xcode.archive</string>
|
||||
<string>com.opa334.trollstore.tipa</string>
|
||||
<string>dyn.ah62d4rv4ge81k4puqe</string>
|
||||
<string>com.google.android.apk</string>
|
||||
<string>dyn.ah62d4rv4ge80c6dp</string>
|
||||
<string>public.archive.apk</string>
|
||||
<string>dyn.ah62d4rv4ge80c6dpry</string>
|
||||
</array>
|
||||
<key>QLThumbnailMinimumDimension</key>
|
||||
<integer>16</integer>
|
||||
|
||||
@@ -16,16 +16,9 @@ extension QLThumbnailReply {
|
||||
}
|
||||
|
||||
class ThumbnailProvider: QLThumbnailProvider {
|
||||
|
||||
// TODO: sadly, this does not seem to work for .xcarchive and .appex
|
||||
// Probably overwritten by Apple somehow
|
||||
|
||||
override func provideThumbnail(for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) {
|
||||
let meta = MetaInfo(request.fileURL)
|
||||
guard let appPlist = meta.readPlistApp() else {
|
||||
return
|
||||
}
|
||||
let img = AppIcon(meta).extractImage(from: appPlist).withRoundCorners()
|
||||
let img = AppIcon(meta).extractImageForThumbnail().withRoundCorners()
|
||||
|
||||
// First way: Draw the thumbnail into the current context, set up with UIKit's coordinate system.
|
||||
let reply = QLThumbnailReply(contextSize: request.maximumSize, currentContextDrawing: { () -> Bool in
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
QLAppBundle
|
||||
===========
|
||||
|
||||
A QuickLook plugin for app bundles (`.ipa`, `.tipa`, `.appex`, `.xcarchive`).
|
||||
A QuickLook plugin for app bundles (`.ipa`, `.tipa`, `.appex`, `.xcarchive`, `.apk`, `.apkm`).
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
## Why?
|
||||
@@ -27,7 +28,7 @@ Also, I've removed support for provisioning profiles (`.mobileprovision`, `.prov
|
||||
|
||||
## ToDo
|
||||
|
||||
- [ ] support for `.apk` files
|
||||
- [x] support for `.apk` files
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,21 @@
|
||||
__ProvisionDeviceIds__
|
||||
</div>
|
||||
|
||||
<div class="__ApkFeaturesRequiredHidden__">
|
||||
<h2>Features (required)</h2>
|
||||
__ApkFeaturesRequiredList__
|
||||
</div>
|
||||
|
||||
<div class="__ApkFeaturesOptionalHidden__">
|
||||
<h2>Features (optional)</h2>
|
||||
__ApkFeaturesOptionalList__
|
||||
</div>
|
||||
|
||||
<div class="__ApkPermissionsHidden__">
|
||||
<h2>Permissions</h2>
|
||||
__ApkPermissionsList__
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>File info</h2>
|
||||
__FileName__<br />
|
||||
|
||||
BIN
screenshot2.png
Normal file
BIN
screenshot2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
@@ -13,6 +13,24 @@ struct AppIcon {
|
||||
self.meta = meta
|
||||
}
|
||||
|
||||
/// Convenience getter to extract app icon regardless of bundle-type.
|
||||
func extractImageForThumbnail() -> NSImage {
|
||||
switch meta.type {
|
||||
case .IPA, .Archive, .Extension:
|
||||
extractImage(from: meta.readPlistApp())
|
||||
case .APK:
|
||||
extractImage(from: meta.readApkIconOnly())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract image from Android app bundle.
|
||||
func extractImage(from manifest: ApkManifest?) -> NSImage {
|
||||
if let data = manifest?.appIconData, let img = NSImage(data: data) {
|
||||
return img
|
||||
}
|
||||
return defaultIcon()
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -22,6 +40,7 @@ struct AppIcon {
|
||||
os_log(.debug, log: log, "[icon] using iTunesArtwork.")
|
||||
return NSImage(data: data)!
|
||||
}
|
||||
// else, fallthrough
|
||||
}
|
||||
|
||||
// Extract image name from app plist
|
||||
@@ -49,6 +68,11 @@ struct AppIcon {
|
||||
}
|
||||
|
||||
// Fallback to default icon
|
||||
return defaultIcon()
|
||||
}
|
||||
|
||||
/// Return the bundled default icon `"defaultIcon.png"`
|
||||
private func defaultIcon() -> NSImage {
|
||||
let iconURL = Bundle.main.url(forResource: "defaultIcon", withExtension: "png")!
|
||||
return NSImage(contentsOf: iconURL)!
|
||||
}
|
||||
@@ -98,13 +122,9 @@ extension AppIcon {
|
||||
var matches: [String] = []
|
||||
switch meta.type {
|
||||
case .IPA:
|
||||
guard let zipFile = meta.zipFile else {
|
||||
// in case unzip in memory is not available, fallback to pattern matching with dynamic suffix
|
||||
return "Payload/*.app/\(iconList.first!)*"
|
||||
}
|
||||
for iconPath in iconList {
|
||||
let zipPath = "Payload/*.app/\(iconPath)*"
|
||||
for zip in zipFile.filesMatching(zipPath) {
|
||||
for zip in meta.zipFile!.filesMatching(zipPath) {
|
||||
if zip.sizeUncompressed > 0 {
|
||||
matches.append(zip.filepath)
|
||||
}
|
||||
@@ -114,6 +134,9 @@ extension AppIcon {
|
||||
}
|
||||
}
|
||||
|
||||
case .APK:
|
||||
return nil // handled in `extractImage()`
|
||||
|
||||
case .Archive, .Extension:
|
||||
for iconPath in iconList {
|
||||
let fileName = iconPath.components(separatedBy: "/").last!
|
||||
|
||||
287
src/MetaInfo+Apk.swift
Normal file
287
src/MetaInfo+Apk.swift
Normal file
@@ -0,0 +1,287 @@
|
||||
import Foundation
|
||||
import AndroidXML
|
||||
import os // OSLog
|
||||
|
||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "MetaInfo+Apk")
|
||||
|
||||
/// Representation of `AndroidManifest.xml`
|
||||
struct ApkManifest {
|
||||
var packageId: String? = nil
|
||||
var appName: String? = nil
|
||||
var appIcon: String? = nil
|
||||
/// Computed property
|
||||
var appIconData: Data? = nil
|
||||
var versionName: String? = nil
|
||||
var versionCode: String? = nil
|
||||
var sdkVerMin: String? = nil
|
||||
var sdkVerTarget: String? = nil
|
||||
|
||||
var featuresRequired: [String] = []
|
||||
var featuresOptional: [String] = []
|
||||
var permissions: [String] = []
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Full Manifest
|
||||
|
||||
extension MetaInfo {
|
||||
/// `(true, <nested-apk>)` -- if extension is `.apkm`.
|
||||
/// `(false, zipFile!)` -- if extension is `.apk`.
|
||||
private func effectiveApk() -> (Bool, ZipFile) {
|
||||
// .apkm may contain multiple .apk files. (plus "icon.png" and "info.json" files)
|
||||
if self.url.pathExtension.lowercased() == "apkm" {
|
||||
if let pth = try? zipFile!.unzipFileToTempDir("base.apk") {
|
||||
return (true, ZipFile(pth))
|
||||
}
|
||||
}
|
||||
// .apk (and derivatives) have their contents structured directly in zip
|
||||
return (false, zipFile!)
|
||||
}
|
||||
|
||||
/// Extract `AndroidManifest.xml` and parse its content
|
||||
func readApkManifest() -> ApkManifest? {
|
||||
assert(type == .APK)
|
||||
let (isApkm, nestedZip) = effectiveApk()
|
||||
guard let data = nestedZip.unzipFile("AndroidManifest.xml") else {
|
||||
return nil
|
||||
}
|
||||
let storage = ApkXmlManifestParser()
|
||||
if let xml = try? AndroidXML.init(data: data) {
|
||||
let ALLOWED_TAGS = [ // keep in sync with `ApkXmlManifestParser`
|
||||
"manifest",
|
||||
"application",
|
||||
"uses-feature",
|
||||
"uses-permission",
|
||||
"uses-permission-sdk-23",
|
||||
"uses-sdk",
|
||||
]
|
||||
let parser = xml.parseXml()
|
||||
let ignore = XMLParser()
|
||||
try? parser.iterElements({ startTag, attributes in
|
||||
if ALLOWED_TAGS.contains(startTag) {
|
||||
storage.parser(ignore, didStartElement: startTag, namespaceURI: nil, qualifiedName: nil, attributes: try attributes.asDictStr())
|
||||
}
|
||||
}) { endTag in
|
||||
if ALLOWED_TAGS.contains(endTag) {
|
||||
storage.parser(ignore, didEndElement: endTag, namespaceURI: nil, qualifiedName: nil)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// fallback to xml-string parser
|
||||
let parser = XMLParser(data: data)
|
||||
parser.delegate = storage
|
||||
parser.parse()
|
||||
}
|
||||
|
||||
var rv = storage.result
|
||||
// if apkm can load png, prefer that over xml-parsing
|
||||
if isApkm, let iconData = zipFile!.unzipFile("icon.png") {
|
||||
rv.appIcon = nil
|
||||
rv.appIconData = iconData
|
||||
}
|
||||
os_log(.debug, log: log, "[apk] resolving %{public}@", String(describing: rv))
|
||||
rv.resolve(nestedZip)
|
||||
os_log(.debug, log: log, "[apk] resolved name: \"%{public}@\" icon: %{public}@", rv.appName ?? "", rv.appIcon ?? "-")
|
||||
return rv
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper to use same code for binary-xml and string-xml parsing
|
||||
private class ApkXmlManifestParser: NSObject, XMLParserDelegate {
|
||||
private var _scope: [String] = []
|
||||
var result = ApkManifest()
|
||||
|
||||
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attrs: [String : String] = [:]) {
|
||||
// keep in sync with ALLOWED_TAGS above
|
||||
switch elementName {
|
||||
case "manifest":
|
||||
if _scope == [] {
|
||||
result.packageId = attrs["package"] // "org.bundle.id"
|
||||
result.versionName = attrs["android:versionName"] // "7.62.3"
|
||||
result.versionCode = attrs["android:versionCode"] // "160700"
|
||||
// attrs["platformBuildVersionCode"] // "35"
|
||||
// attrs["platformBuildVersionName"] // "15"
|
||||
}
|
||||
case "application":
|
||||
if _scope == ["manifest"] {
|
||||
result.appName = attrs["android:label"] // @resource-ref
|
||||
result.appIcon = attrs["android:icon"] // @resource-ref
|
||||
}
|
||||
case "uses-permission", "uses-permission-sdk-23":
|
||||
// no "permission" because that will produce duplicates with "uses-permission"
|
||||
if _scope == ["manifest"], let name = attrs["android:name"] {
|
||||
result.permissions.append(name)
|
||||
}
|
||||
case "uses-feature":
|
||||
if _scope == ["manifest"], let name = attrs["android:name"] {
|
||||
if attrs["android:required"] == "false" {
|
||||
result.featuresOptional.append(name)
|
||||
} else {
|
||||
result.featuresRequired.append(name)
|
||||
}
|
||||
}
|
||||
case "uses-sdk":
|
||||
if _scope == ["manifest"] {
|
||||
result.sdkVerMin = attrs["android:minSdkVersion"] ?? "1" // "21"
|
||||
result.sdkVerTarget = attrs["android:targetSdkVersion"] // "35"
|
||||
}
|
||||
default: break // ignore
|
||||
}
|
||||
_scope.append(elementName)
|
||||
}
|
||||
|
||||
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
|
||||
_scope.removeLast()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Icon only
|
||||
|
||||
extension MetaInfo {
|
||||
/// Same as `readApkManifest()` but only extract `appIcon`.
|
||||
func readApkIconOnly() -> ApkManifest? {
|
||||
assert(type == .APK)
|
||||
var rv = ApkManifest()
|
||||
let (isApkm, nestedZip) = effectiveApk()
|
||||
if isApkm, let iconData = zipFile!.unzipFile("icon.png") {
|
||||
rv.appIcon = "icon.png"
|
||||
rv.appIconData = iconData
|
||||
return rv
|
||||
}
|
||||
guard let data = nestedZip.unzipFile("AndroidManifest.xml") else {
|
||||
return nil
|
||||
}
|
||||
if let xml = try? AndroidXML.init(data: data) {
|
||||
let parser = xml.parseXml()
|
||||
try? parser.iterElements({ startTag, attributes in
|
||||
if startTag == "application" {
|
||||
rv.appIcon = try? attributes.get("android:icon")?.resolve(parser.stringPool)
|
||||
}
|
||||
}) {_ in}
|
||||
} else {
|
||||
// fallback to xml-string parser
|
||||
rv.appIcon = ApkXmlIconParser().run(data)
|
||||
}
|
||||
rv.resolve(nestedZip)
|
||||
return rv
|
||||
}
|
||||
}
|
||||
|
||||
/// Shorter form of `ApkXmlManifestParser` to only exctract the icon reference (used for Thumbnail Provider)
|
||||
private class ApkXmlIconParser: NSObject, XMLParserDelegate {
|
||||
var result: String? = nil
|
||||
|
||||
func run(_ data: Data) -> String? {
|
||||
let parser = XMLParser(data: data)
|
||||
parser.delegate = self
|
||||
parser.parse()
|
||||
return result
|
||||
}
|
||||
|
||||
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attrs: [String : String] = [:]) {
|
||||
if elementName == "application" {
|
||||
result = attrs["android:icon"]
|
||||
parser.abortParsing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Resolve resource
|
||||
|
||||
private extension ApkManifest {
|
||||
/// Reuse `ZipFile` from previous call because that may be an already unpacked `base.apk`
|
||||
mutating func resolve(_ zip: ZipFile) {
|
||||
guard let data = zip.unzipFile("resources.arsc"),
|
||||
let xml = try? AndroidXML.init(data: data), xml.type == .Table else {
|
||||
return
|
||||
}
|
||||
|
||||
let parser = xml.parseTable()
|
||||
if let val = appName, let ref = try? TblTableRef(val) {
|
||||
appName = parser.getName(ref)
|
||||
}
|
||||
if let val = appIcon, let ref = try? TblTableRef(val) {
|
||||
if let iconPath = parser.getIconDirect(ref) ?? parser.getIconIndirect(ref) {
|
||||
appIcon = iconPath
|
||||
appIconData = zip.unzipFile(iconPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Tbl_Parser {
|
||||
func getName(_ ref: TblTableRef) -> String? {
|
||||
// why the heck are these even allowed?
|
||||
// apparently there can be references onto references
|
||||
var ref = ref
|
||||
while let res = try? self.getResource(ref) {
|
||||
guard let val = res.entries.first?.entry.value else {
|
||||
return nil
|
||||
}
|
||||
switch val.dataType {
|
||||
case .Reference: ref = val.asTableRef // and continue
|
||||
case .String: return val.resolve(self.stringPool)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Lookup resource with matching id. Choose the icon with the highest density.
|
||||
func getIconDirect(_ ref: TblTableRef) -> String? {
|
||||
guard let res = try? self.getResource(ref) else {
|
||||
return nil
|
||||
}
|
||||
var best: ResValue? = nil
|
||||
var bestScore: UInt16 = 0
|
||||
for e in res.entries {
|
||||
switch e.config.screenType.density {
|
||||
case .Default, .any, .None: continue
|
||||
case let density:
|
||||
if density.rawValue > bestScore, let val = e.entry.value {
|
||||
bestScore = density.rawValue
|
||||
best = val
|
||||
}
|
||||
}
|
||||
}
|
||||
return best?.resolve(self.stringPool)
|
||||
}
|
||||
|
||||
/// Iterate over all entries and choose best-rated icon file.
|
||||
/// Rating prefers files which have an attribute name `"app_icon"` or `"ic_launcher"`.
|
||||
func getIconIndirect(_ ref: TblTableRef) -> String? {
|
||||
// sadly we cannot just `getResource()` because that can point to an app banner
|
||||
guard let pkg = try? self.getPackage(ref.package),
|
||||
var pool = pkg.stringPool(for: .Keys),
|
||||
let (_, types) = try? pkg.getType(ref.type) else {
|
||||
return nil
|
||||
}
|
||||
// density is 120-640
|
||||
let rates: [String: UInt16] = [
|
||||
"app_icon": 1000,
|
||||
"ic_launcher": 800,
|
||||
"ic_launcher_foreground": 200,
|
||||
]
|
||||
var best: ResValue? = nil
|
||||
var bestScore: UInt16 = 0
|
||||
for typ in types {
|
||||
switch typ.config.screenType.density {
|
||||
case .any, .None: continue
|
||||
case let density:
|
||||
try? typ.iterValues {
|
||||
if let val = $1.value {
|
||||
let attrName = pool.getStringCached($1.key)
|
||||
let score = density.rawValue + (rates[attrName] ?? 0)
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
best = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return best?.resolve(self.stringPool)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ enum FileType {
|
||||
case IPA
|
||||
case Archive
|
||||
case Extension
|
||||
case APK
|
||||
}
|
||||
|
||||
struct MetaInfo {
|
||||
@@ -25,14 +26,14 @@ struct MetaInfo {
|
||||
/// Use file url and UTI type to generate an info object to pass around.
|
||||
init(_ url: URL) {
|
||||
self.url = url
|
||||
self.UTI = try! url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier ?? "Unknown"
|
||||
self.UTI = try! url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier ?? "Unknown"
|
||||
|
||||
var isOSX = false
|
||||
var effective: URL? = nil
|
||||
var zipFile: ZipFile? = nil
|
||||
|
||||
switch self.UTI {
|
||||
case "com.apple.itunes.ipa", "com.opa334.trollstore.tipa", "dyn.ah62d4rv4ge81k4puqe":
|
||||
case "com.apple.itunes.ipa", "com.opa334.trollstore.tipa", "dyn.ah62d4rv4ge81k4puqe" /* tipa */:
|
||||
self.type = FileType.IPA
|
||||
zipFile = ZipFile(self.url.path)
|
||||
case "com.apple.xcode.archive":
|
||||
@@ -46,6 +47,9 @@ struct MetaInfo {
|
||||
}
|
||||
case "com.apple.application-and-system-extension":
|
||||
self.type = FileType.Extension
|
||||
case "com.google.android.apk", "dyn.ah62d4rv4ge80c6dp" /* apk */, "public.archive.apk", "dyn.ah62d4rv4ge80c6dpry" /* apkm */:
|
||||
self.type = FileType.APK
|
||||
zipFile = ZipFile(self.url.path)
|
||||
default:
|
||||
os_log(.error, log: log, "Unsupported file type: %{public}@", self.UTI)
|
||||
fatalError()
|
||||
@@ -58,7 +62,7 @@ struct MetaInfo {
|
||||
/// Evaluate path with `osxSubdir` and `filename`
|
||||
func effectiveUrl(_ osxSubdir: String?, _ filename: String) -> URL {
|
||||
switch self.type {
|
||||
case .IPA:
|
||||
case .IPA, .APK:
|
||||
return effectiveUrl
|
||||
case .Archive, .Extension:
|
||||
if isOSX, let osxSubdir {
|
||||
@@ -75,6 +79,8 @@ struct MetaInfo {
|
||||
switch self.type {
|
||||
case .IPA:
|
||||
return zipFile!.unzipFile("Payload/*.app/".appending(filename))
|
||||
case .APK:
|
||||
return nil // not applicable for .apk
|
||||
case .Archive, .Extension:
|
||||
return try? Data(contentsOf: self.effectiveUrl(osxSubdir, filename))
|
||||
}
|
||||
@@ -85,6 +91,8 @@ struct MetaInfo {
|
||||
switch self.type {
|
||||
case .IPA, .Archive, .Extension:
|
||||
return self.readPayloadFile("Info.plist", osxSubdir: nil)?.asPlistOrNil()
|
||||
case .APK:
|
||||
return nil // not applicable for Android
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,7 @@ extension PreviewGenerator {
|
||||
}
|
||||
|
||||
/// Process info stored in `Info.plist`
|
||||
mutating func procAppInfo(_ appPlist: PlistDict?, isOSX: Bool) {
|
||||
guard let appPlist else {
|
||||
self.apply(["AppInfoHidden": CLASS_HIDDEN])
|
||||
return
|
||||
}
|
||||
mutating func procAppInfoApple(_ appPlist: PlistDict, isOSX: Bool) {
|
||||
let minVersion = appPlist[isOSX ? "LSMinimumSystemVersion" : "MinimumOSVersion"] as? String ?? ""
|
||||
|
||||
let extensionType = (appPlist["NSExtension"] as? PlistDict)?["NSExtensionPointIdentifier"] as? String
|
||||
@@ -46,4 +42,81 @@ extension PreviewGenerator {
|
||||
"AppMinOS": minVersion,
|
||||
])
|
||||
}
|
||||
|
||||
/// Process info stored in `Info.plist`
|
||||
mutating func procAppInfoAndroid(_ manifest: ApkManifest) {
|
||||
let featReq = manifest.featuresRequired
|
||||
let featOpt = manifest.featuresOptional
|
||||
let perms = manifest.permissions
|
||||
|
||||
func asList(_ list: [String]) -> String {
|
||||
"<pre>\(list.joined(separator: "\n"))</pre>"
|
||||
}
|
||||
|
||||
func resolveSDK(_ sdk: String?) -> String {
|
||||
sdk == nil ? "" : "\(sdk!) (Android \(ANDROID_SDK_MAP[sdk!] ?? "?"))"
|
||||
}
|
||||
self.apply([
|
||||
"AppInfoHidden": CLASS_VISIBLE,
|
||||
"AppName": manifest.appName ?? "",
|
||||
"AppVersion": manifest.versionName ?? "",
|
||||
"AppBuildVer": manifest.versionCode ?? "",
|
||||
"AppId": manifest.packageId ?? "",
|
||||
|
||||
"ApkFeaturesRequiredHidden": featReq.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||
"ApkFeaturesRequiredList": asList(featReq),
|
||||
"ApkFeaturesOptionalHidden": featOpt.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||
"ApkFeaturesOptionalList": asList(featOpt),
|
||||
"ApkPermissionsHidden": perms.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||
"ApkPermissionsList": asList(perms),
|
||||
|
||||
"AppDeviceFamily": "Android",
|
||||
"AppSDK": resolveSDK(manifest.sdkVerTarget),
|
||||
"AppMinOS": resolveSDK(manifest.sdkVerMin),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private let ANDROID_SDK_MAP: [String: String] = [
|
||||
"1": "1.0",
|
||||
"2": "1.1",
|
||||
"3": "1.5",
|
||||
"4": "1.6",
|
||||
"5": "2.0",
|
||||
"6": "2.0.1",
|
||||
"7": "2.1.x",
|
||||
"8": "2.2.x",
|
||||
"9": "2.3, 2.3.1, 2.3.2",
|
||||
"10": "2.3.3, 2.3.4",
|
||||
"11": "3.0.x",
|
||||
"12": "3.1.x",
|
||||
"13": "3.2",
|
||||
"14": "4.0, 4.0.1, 4.0.2",
|
||||
"15": "4.0.3, 4.0.4",
|
||||
"16": "4.1, 4.1.1",
|
||||
"17": "4.2, 4.2.2",
|
||||
"18": "4.3",
|
||||
"19": "4.4",
|
||||
"20": "4.4W",
|
||||
"21": "5.0",
|
||||
"22": "5.1",
|
||||
"23": "6.0",
|
||||
"24": "7.0",
|
||||
"25": "7.1, 7.1.1",
|
||||
"26": "8.0",
|
||||
"27": "8.1",
|
||||
"28": "9",
|
||||
"29": "10",
|
||||
"30": "11",
|
||||
"31": "12",
|
||||
"32": "12",
|
||||
"33": "13",
|
||||
"34": "14",
|
||||
"35": "15",
|
||||
"36": "16",
|
||||
// can we assume new versions will stick to this scheme?
|
||||
"37": "17",
|
||||
"38": "18",
|
||||
"39": "19",
|
||||
"40": "20",
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@ extension MetaInfo {
|
||||
case .Archive:
|
||||
// not `readPayloadFile` because plist is in root dir
|
||||
return try? Data(contentsOf: self.url.appendingPathComponent("Info.plist", isDirectory: false)).asPlistOrNil()
|
||||
case .IPA, .Extension:
|
||||
case .IPA, .Extension, .APK:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ extension PreviewGenerator {
|
||||
/// Process info of `.xcarchive` stored in root `Info.plist`
|
||||
mutating func procArchiveInfo(_ archivePlist: PlistDict?) {
|
||||
guard let archivePlist, let comment = archivePlist["Comment"] as? String else {
|
||||
self.apply(["ArchiveHidden": CLASS_HIDDEN])
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -3,22 +3,22 @@ import Foundation
|
||||
extension PreviewGenerator {
|
||||
/// Search for app binary and run `codesign` on it.
|
||||
private func readEntitlements(_ meta: MetaInfo, _ bundleExecutable: String?) -> Entitlements {
|
||||
guard let bundleExecutable else {
|
||||
return Entitlements.withoutBinary()
|
||||
}
|
||||
|
||||
switch meta.type {
|
||||
case .IPA:
|
||||
let tmpPath = NSTemporaryDirectory() + "/" + UUID().uuidString
|
||||
try! FileManager.default.createDirectory(atPath: tmpPath, withIntermediateDirectories: true)
|
||||
defer {
|
||||
try? FileManager.default.removeItem(atPath: tmpPath)
|
||||
if let exe = bundleExecutable {
|
||||
switch meta.type {
|
||||
case .IPA:
|
||||
if let tmpPath = try? meta.zipFile!.unzipFileToTempDir("Payload/*.app/\(exe)") {
|
||||
defer {
|
||||
try? FileManager.default.removeItem(atPath: tmpPath)
|
||||
}
|
||||
return Entitlements(forBinary: tmpPath + "/" + exe)
|
||||
}
|
||||
case .Archive, .Extension:
|
||||
return Entitlements(forBinary: meta.effectiveUrl("MacOS", exe).path)
|
||||
case .APK:
|
||||
break // not applicable for Android
|
||||
}
|
||||
try! meta.zipFile!.unzipFile("Payload/*.app/\(bundleExecutable)", toDir: tmpPath)
|
||||
return Entitlements(forBinary: tmpPath + "/" + bundleExecutable)
|
||||
case .Archive, .Extension:
|
||||
return Entitlements(forBinary: meta.effectiveUrl("MacOS", bundleExecutable).path)
|
||||
}
|
||||
return Entitlements.withoutBinary()
|
||||
}
|
||||
|
||||
/// Process compiled binary and provision plist to extract `Entitlements`
|
||||
@@ -27,7 +27,6 @@ extension PreviewGenerator {
|
||||
entitlements.applyFallbackIfNeeded(provisionPlist?["Entitlements"] as? PlistDict)
|
||||
|
||||
if entitlements.html == nil && !entitlements.hasError {
|
||||
self.apply(["EntitlementsHidden" : CLASS_HIDDEN])
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,6 @@ extension PreviewGenerator {
|
||||
/// Process info stored in `embedded.mobileprovision`
|
||||
mutating func procProvision(_ provisionPlist: PlistDict?, isOSX: Bool) {
|
||||
guard let provisionPlist else {
|
||||
self.apply(["ProvisionHidden": CLASS_HIDDEN])
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ extension PreviewGenerator {
|
||||
/// Process ATS info in `Info.plist`
|
||||
mutating func procTransportSecurity(_ appPlist: PlistDict?) {
|
||||
guard let value = appPlist?["NSAppTransportSecurity"] as? PlistDict else {
|
||||
self.apply(["TransportSecurityHidden": CLASS_HIDDEN])
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ extension MetaInfo {
|
||||
case .IPA:
|
||||
// not `readPayloadFile` because plist is in root dir
|
||||
return self.zipFile!.unzipFile("iTunesMetadata.plist")?.asPlistOrNil()
|
||||
case .Archive, .Extension:
|
||||
case .Archive, .Extension, .APK:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ extension PreviewGenerator {
|
||||
/// Process info stored in `iTunesMetadata.plist`
|
||||
mutating func procItunesMeta(_ itunesPlist: PlistDict?) {
|
||||
guard let itunesPlist else {
|
||||
self.apply(["iTunesHidden": CLASS_HIDDEN])
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -4,28 +4,57 @@ let CLASS_HIDDEN = "hidden"
|
||||
let CLASS_VISIBLE = ""
|
||||
|
||||
struct PreviewGenerator {
|
||||
var data: [String: String] = [:] // used for TAG replacements
|
||||
/// Used for TAG replacements
|
||||
var data: [String: String] = [
|
||||
// default: hide everything
|
||||
"AppInfoHidden": CLASS_HIDDEN,
|
||||
"AppExtensionTypeHidden": CLASS_HIDDEN,
|
||||
"ArchiveHidden": CLASS_HIDDEN,
|
||||
"iTunesHidden": CLASS_HIDDEN,
|
||||
"TransportSecurityHidden": CLASS_HIDDEN,
|
||||
"EntitlementsHidden": CLASS_HIDDEN,
|
||||
"EntitlementsWarningHidden": CLASS_HIDDEN,
|
||||
"ProvisionHidden": CLASS_HIDDEN,
|
||||
"ApkFeaturesRequiredHidden": CLASS_HIDDEN,
|
||||
"ApkFeaturesOptionalHidden": CLASS_HIDDEN,
|
||||
"ApkPermissionsHidden": CLASS_HIDDEN,
|
||||
]
|
||||
let meta: MetaInfo
|
||||
|
||||
init(_ meta: MetaInfo) throws {
|
||||
self.meta = meta
|
||||
guard let plistApp = meta.readPlistApp() else {
|
||||
throw RuntimeError("Info.plist not found")
|
||||
|
||||
switch meta.type {
|
||||
case .IPA, .Archive, .Extension:
|
||||
guard let plistApp = meta.readPlistApp() else {
|
||||
throw RuntimeError("Info.plist not found")
|
||||
}
|
||||
procAppInfoApple(plistApp, isOSX: meta.isOSX)
|
||||
if meta.type == .IPA {
|
||||
procItunesMeta(meta.readPlistItunes())
|
||||
} else if meta.type == .Archive {
|
||||
procArchiveInfo(meta.readPlistXCArchive())
|
||||
}
|
||||
procTransportSecurity(plistApp)
|
||||
|
||||
let plistProvision = meta.readPlistProvision()
|
||||
procEntitlements(meta, plistApp, plistProvision)
|
||||
procProvision(plistProvision, isOSX: meta.isOSX)
|
||||
// App Icon (last, because the image uses a lot of memory)
|
||||
data["AppIcon"] = AppIcon(meta).extractImage(from: plistApp).withRoundCorners().asBase64()
|
||||
|
||||
case .APK:
|
||||
guard let manifest = meta.readApkManifest() else {
|
||||
throw RuntimeError("AndroidManifest.xml not found")
|
||||
}
|
||||
procAppInfoAndroid(manifest)
|
||||
// App Icon (last, because the image uses a lot of memory)
|
||||
data["AppIcon"] = AppIcon(meta).extractImage(from: manifest).withRoundCorners().asBase64()
|
||||
}
|
||||
let plistProvision = meta.readPlistProvision()
|
||||
|
||||
data["QuickLookTitle"] = stringForFileType(meta)
|
||||
|
||||
procAppInfo(plistApp, isOSX: meta.isOSX)
|
||||
procArchiveInfo(meta.readPlistXCArchive())
|
||||
procItunesMeta(meta.readPlistItunes())
|
||||
procTransportSecurity(plistApp)
|
||||
procEntitlements(meta, plistApp, plistProvision)
|
||||
procProvision(plistProvision, isOSX: meta.isOSX)
|
||||
procFileInfo(meta.url)
|
||||
procFooterInfo()
|
||||
// App Icon (last, because the image uses a lot of memory)
|
||||
data["AppIcon"] = AppIcon(meta).extractImage(from: plistApp).withRoundCorners().asBase64()
|
||||
}
|
||||
|
||||
mutating func apply(_ values: [String: String]) {
|
||||
@@ -35,7 +64,7 @@ struct PreviewGenerator {
|
||||
/// Title of the preview window
|
||||
private func stringForFileType(_ meta: MetaInfo) -> String {
|
||||
switch meta.type {
|
||||
case .IPA: return "App info"
|
||||
case .IPA, .APK: return "App info"
|
||||
case .Archive: return "Archive info"
|
||||
case .Extension: return "App extension info"
|
||||
}
|
||||
|
||||
@@ -354,12 +354,23 @@ struct ZipFile {
|
||||
/// Unzip file to filesystem.
|
||||
/// @param filePath File path inside zip file.
|
||||
/// @param targetDir Directory in which to unzip the file.
|
||||
func unzipFile(_ filePath: String, toDir targetDir: String) throws {
|
||||
if let data = self.unzipFile(filePath) {
|
||||
let filename = filePath.components(separatedBy: "/").last!
|
||||
let outputPath = targetDir.appending("/" + filename)
|
||||
os_log(.debug, log: log, "[unzip] write to %{public}@", outputPath)
|
||||
try data.write(to: URL(fileURLWithPath: outputPath), options: .atomic)
|
||||
@discardableResult
|
||||
func unzipFile(_ filePath: String, toDir targetDir: String) throws -> String? {
|
||||
guard let data = self.unzipFile(filePath) else {
|
||||
return nil
|
||||
}
|
||||
let filename = filePath.components(separatedBy: "/").last!
|
||||
let outputPath = targetDir.appending("/" + filename)
|
||||
os_log(.debug, log: log, "[unzip] write to %{public}@", outputPath)
|
||||
try data.write(to: URL(fileURLWithPath: outputPath), options: .atomic)
|
||||
return outputPath
|
||||
}
|
||||
|
||||
/// Extract selected `filePath` inside zip to a new temporary directory and return path to that file.
|
||||
/// @return Path to extracted data. Returns `nil` or throws exception if data could not be extracted.
|
||||
func unzipFileToTempDir(_ filePath: String) throws -> String? {
|
||||
let tmpPath = NSTemporaryDirectory() + "/" + UUID().uuidString
|
||||
try! FileManager.default.createDirectory(atPath: tmpPath, withIntermediateDirectories: true)
|
||||
return try unzipFile(filePath, toDir: tmpPath)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user