feat: support for apkm

This commit is contained in:
relikd
2025-11-29 20:24:29 +01:00
parent 6bb62bdf82
commit 3a587ce730
5 changed files with 36 additions and 8 deletions

View File

@@ -43,6 +43,7 @@
<key>public.filename-extension</key> <key>public.filename-extension</key>
<array> <array>
<string>apk</string> <string>apk</string>
<string>apkm</string>
</array> </array>
</dict> </dict>
</dict> </dict>

View File

@@ -18,6 +18,7 @@
<string>com.google.android.apk</string> <string>com.google.android.apk</string>
<string>dyn.ah62d4rv4ge80c6dp</string> <string>dyn.ah62d4rv4ge80c6dp</string>
<string>public.archive.apk</string> <string>public.archive.apk</string>
<string>dyn.ah62d4rv4ge80c6dpry</string>
</array> </array>
<key>QLSupportsSearchableItems</key> <key>QLSupportsSearchableItems</key>
<false/> <false/>

View File

@@ -16,6 +16,7 @@
<string>com.google.android.apk</string> <string>com.google.android.apk</string>
<string>dyn.ah62d4rv4ge80c6dp</string> <string>dyn.ah62d4rv4ge80c6dp</string>
<string>public.archive.apk</string> <string>public.archive.apk</string>
<string>dyn.ah62d4rv4ge80c6dpry</string>
</array> </array>
<key>QLThumbnailMinimumDimension</key> <key>QLThumbnailMinimumDimension</key>
<integer>16</integer> <integer>16</integer>

View File

@@ -25,10 +25,24 @@ struct ApkManifest {
// MARK: - Full Manifest // MARK: - Full Manifest
extension MetaInfo { 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 /// Extract `AndroidManifest.xml` and parse its content
func readApkManifest() -> ApkManifest? { func readApkManifest() -> ApkManifest? {
assert(type == .APK) assert(type == .APK)
guard let data = self.readPayloadFile("AndroidManifest.xml", osxSubdir: nil) else { let (isApkm, nestedZip) = effectiveApk()
guard let data = nestedZip.unzipFile("AndroidManifest.xml") else {
return nil return nil
} }
let storage = ApkXmlManifestParser() let storage = ApkXmlManifestParser()
@@ -60,8 +74,13 @@ extension MetaInfo {
} }
var rv = storage.result 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)) os_log(.debug, log: log, "[apk] resolving %{public}@", String(describing: rv))
rv.resolve(zipFile!) rv.resolve(nestedZip)
os_log(.debug, log: log, "[apk] resolved name: \"%{public}@\" icon: %{public}@", rv.appName ?? "", rv.appIcon ?? "-") os_log(.debug, log: log, "[apk] resolved name: \"%{public}@\" icon: %{public}@", rv.appName ?? "", rv.appIcon ?? "-")
return rv return rv
} }
@@ -124,7 +143,13 @@ extension MetaInfo {
func readApkIconOnly() -> ApkManifest? { func readApkIconOnly() -> ApkManifest? {
assert(type == .APK) assert(type == .APK)
var rv = ApkManifest() var rv = ApkManifest()
guard let data = self.readPayloadFile("AndroidManifest.xml", osxSubdir: nil) else { 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 return nil
} }
if let xml = try? AndroidXML.init(data: data) { if let xml = try? AndroidXML.init(data: data) {
@@ -138,7 +163,7 @@ extension MetaInfo {
// fallback to xml-string parser // fallback to xml-string parser
rv.appIcon = ApkXmlIconParser().run(data) rv.appIcon = ApkXmlIconParser().run(data)
} }
rv.resolve(zipFile!) rv.resolve(nestedZip)
return rv return rv
} }
} }
@@ -166,6 +191,7 @@ private class ApkXmlIconParser: NSObject, XMLParserDelegate {
// MARK: - Resolve resource // MARK: - Resolve resource
private extension ApkManifest { private extension ApkManifest {
/// Reuse `ZipFile` from previous call because that may be an already unpacked `base.apk`
mutating func resolve(_ zip: ZipFile) { mutating func resolve(_ zip: ZipFile) {
guard let data = zip.unzipFile("resources.arsc"), guard let data = zip.unzipFile("resources.arsc"),
let xml = try? AndroidXML.init(data: data), xml.type == .Table else { let xml = try? AndroidXML.init(data: data), xml.type == .Table else {

View File

@@ -33,7 +33,7 @@ struct MetaInfo {
var zipFile: ZipFile? = nil var zipFile: ZipFile? = nil
switch self.UTI { 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 self.type = FileType.IPA
zipFile = ZipFile(self.url.path) zipFile = ZipFile(self.url.path)
case "com.apple.xcode.archive": case "com.apple.xcode.archive":
@@ -47,10 +47,9 @@ struct MetaInfo {
} }
case "com.apple.application-and-system-extension": case "com.apple.application-and-system-extension":
self.type = FileType.Extension self.type = FileType.Extension
case "com.google.android.apk", "dyn.ah62d4rv4ge80c6dp", "public.archive.apk": case "com.google.android.apk", "dyn.ah62d4rv4ge80c6dp" /* apk */, "public.archive.apk", "dyn.ah62d4rv4ge80c6dpry" /* apkm */:
self.type = FileType.APK self.type = FileType.APK
zipFile = ZipFile(self.url.path) zipFile = ZipFile(self.url.path)
// case "com.google.android.apkm", "dyn.ah62d4rv4ge80c6dpry":
default: default:
os_log(.error, log: log, "Unsupported file type: %{public}@", self.UTI) os_log(.error, log: log, "Unsupported file type: %{public}@", self.UTI)
fatalError() fatalError()
@@ -81,7 +80,7 @@ struct MetaInfo {
case .IPA: case .IPA:
return zipFile!.unzipFile("Payload/*.app/".appending(filename)) return zipFile!.unzipFile("Payload/*.app/".appending(filename))
case .APK: case .APK:
return zipFile!.unzipFile(filename) return nil // not applicable for .apk
case .Archive, .Extension: case .Archive, .Extension:
return try? Data(contentsOf: self.effectiveUrl(osxSubdir, filename)) return try? Data(contentsOf: self.effectiveUrl(osxSubdir, filename))
} }