Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15f536cb79 | ||
|
|
ac8b76d3fc | ||
|
|
62d1407d17 | ||
|
|
eb169ae0a2 | ||
|
|
abdee3b780 | ||
|
|
38c861442c | ||
|
|
1ce5f3e069 | ||
|
|
d0da644c26 | ||
|
|
76e7e22b49 | ||
|
|
72e395c5da | ||
|
|
be65aaa19a | ||
|
|
feae5aba3e | ||
|
|
3a587ce730 | ||
|
|
6bb62bdf82 | ||
|
|
f233d0e4a2 | ||
|
|
035276dcfc | ||
|
|
3a16277867 | ||
|
|
591a75dabc | ||
|
|
cde957b01f | ||
|
|
71d1b35aac | ||
|
|
5cd7034fc8 | ||
|
|
e0ccba1af8 | ||
|
|
21c21ec059 | ||
|
|
cfb6b17bc7 | ||
|
|
2d16cb666b | ||
|
|
1a6d98a4b2 | ||
|
|
85c1ae95c1 | ||
|
|
fd13f13a3c | ||
|
|
8b916829d1 | ||
|
|
05f30ee755 | ||
|
|
5166a67e48 | ||
|
|
d1aae4cc15 | ||
|
|
f38c1f802f | ||
|
|
af9c398571 | ||
|
|
36e30a1fdf | ||
|
|
fb8fa41dd0 | ||
|
|
33cec015ab | ||
|
|
6dec6530c5 |
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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>
|
||||
|
||||
@@ -25,7 +25,7 @@ public class CarReader {
|
||||
return NSImage(cgImage: bestImage.image, size: bestImage.size)
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ public class CarReader {
|
||||
os_log(.debug, log: log, "[asset-car] available keys: %{public}@", catalog.allImageNames() ?? [])
|
||||
return nil
|
||||
}
|
||||
return imageName;
|
||||
return imageName
|
||||
}
|
||||
|
||||
/// If exact name does not exist in catalog, search for a name that shares the same prefix.
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -5,6 +5,33 @@ 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.5.0] – 2025-12-01
|
||||
Fixed:
|
||||
- `.appex` macOS extensions (`Info.plist` was not found)
|
||||
- Removed `.appex` from `ThumbnailProvider` (extension probably dont have icons anyway)
|
||||
|
||||
Changed:
|
||||
- Template uses `{{X}}` instead of `__X__`
|
||||
|
||||
|
||||
## [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`
|
||||
- Show `.xcarchive` developer notes
|
||||
|
||||
Fixed:
|
||||
- Cancel preview (and allow other plugins to run) if there is no `Info.plist` in `.xcarchive`
|
||||
|
||||
Changed:
|
||||
- Hide Transport Security and Entitlements if they are empty
|
||||
|
||||
|
||||
## [1.2.0] – 2025-11-04
|
||||
Added:
|
||||
- Customizable HTML template
|
||||
@@ -25,6 +52,9 @@ Added:
|
||||
Initial release
|
||||
|
||||
|
||||
[1.5.0]: https://github.com/relikd/QLAppBundle/compare/v1.4.0...v1.5.0
|
||||
[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
|
||||
[1.0.0]: https://github.com/relikd/QLAppBundle/compare/9b0761318c85090d1ef22f12d3eab67a9a194882...v1.0.0
|
||||
|
||||
@@ -8,6 +8,7 @@ ENABLE_HARDENED_RUNTIME = YES
|
||||
SWIFT_VERSION = 5.0
|
||||
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15
|
||||
MARKETING_VERSION = 1.2.0
|
||||
MARKETING_VERSION = 1.5.0
|
||||
PRODUCT_NAME = QLAppBundle
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLAppBundle
|
||||
CURRENT_PROJECT_VERSION = 2018
|
||||
|
||||
@@ -9,13 +9,21 @@
|
||||
/* 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 /* Apk+Manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540B77D82ED79BB2009E030C /* Apk+Manifest.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 */; };
|
||||
543899082EDCA223007C02FC /* Apk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543899072EDCA223007C02FC /* Apk.swift */; };
|
||||
543899092EDCA26D007C02FC /* Apk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543899072EDCA223007C02FC /* Apk.swift */; };
|
||||
5438990B2EDCA27F007C02FC /* Apk+Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5438990A2EDCA27F007C02FC /* Apk+Icon.swift */; };
|
||||
5438990E2EDCB126007C02FC /* Apk+Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5438990A2EDCA27F007C02FC /* Apk+Icon.swift */; };
|
||||
543FE5742EB3BB5E0059F98B /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 543FE5732EB3BB5E0059F98B /* AppIcon.icns */; };
|
||||
54442C232E378BAF008A870E /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54442C222E378BAF008A870E /* Quartz.framework */; };
|
||||
54442C702E378BDD008A870E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54442C6A2E378BDD008A870E /* AppDelegate.swift */; };
|
||||
54442C722E378BDD008A870E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54442C6D2E378BDD008A870E /* MainMenu.xib */; };
|
||||
54442C792E378BE0008A870E /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54442C742E378BE0008A870E /* PreviewViewController.swift */; };
|
||||
54442C7B2E378BE0008A870E /* PreviewViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54442C762E378BE0008A870E /* PreviewViewController.xib */; };
|
||||
544AF3692EB6AAC0006837F2 /* AssetCarReader.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 544AF3682EB6AAC0006837F2 /* AssetCarReader.xcconfig */; };
|
||||
545459C42EA469E4002892E5 /* defaultIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 54D3A6F22EA4603B001EF4F6 /* defaultIcon.png */; };
|
||||
545459C52EA469EA002892E5 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 54D3A6F32EA4603B001EF4F6 /* template.html */; };
|
||||
54581FD12EB29A0B0043A0B3 /* QuickLookThumbnailing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54581FD02EB29A0B0043A0B3 /* QuickLookThumbnailing.framework */; };
|
||||
@@ -36,14 +44,24 @@
|
||||
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 */; };
|
||||
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, ); }; };
|
||||
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 */; };
|
||||
549E3BA42EBC021500ADFF56 /* Preview+TransportSecurity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E3BA32EBC021500ADFF56 /* Preview+TransportSecurity.swift */; };
|
||||
54AE5BFF2EB3DB1000B4CFC7 /* ThumbnailProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AE5BFD2EB3DB1000B4CFC7 /* ThumbnailProvider.swift */; };
|
||||
54B6FFEE2EB6A847007397C0 /* AssetCarReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B6FFEC2EB6A847007397C0 /* AssetCarReader.swift */; };
|
||||
54B6FFEF2EB6A8E0007397C0 /* CoreUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54D3A6F52EA4610B001EF4F6 /* CoreUI.framework */; };
|
||||
54B6FFF02EB6AA0F007397C0 /* AssetCarReader.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54352E8A2EB6A79A0082F61D /* AssetCarReader.framework */; };
|
||||
54B6FFF12EB6AA0F007397C0 /* AssetCarReader.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54352E8A2EB6A79A0082F61D /* AssetCarReader.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
54B6FFF12EB6AA0F007397C0 /* AssetCarReader.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54352E8A2EB6A79A0082F61D /* AssetCarReader.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
54B6FFF52EB6AA14007397C0 /* AssetCarReader.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54352E8A2EB6A79A0082F61D /* AssetCarReader.framework */; };
|
||||
54B6FFF62EB6AA14007397C0 /* AssetCarReader.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54352E8A2EB6A79A0082F61D /* AssetCarReader.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
54B6FFF62EB6AA14007397C0 /* AssetCarReader.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54352E8A2EB6A79A0082F61D /* AssetCarReader.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
54CCF59E2EDC9A6800D766F9 /* AndroidSdkMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CCF59D2EDC9A6800D766F9 /* AndroidSdkMap.swift */; };
|
||||
54D3A6EC2EA31B52001EF4F6 /* AppCategories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D3A6EB2EA31B52001EF4F6 /* AppCategories.swift */; };
|
||||
54D3A6EE2EA39CC6001EF4F6 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D3A6ED2EA39CC6001EF4F6 /* AppIcon.swift */; };
|
||||
54D3A6F02EA3F49F001EF4F6 /* NSBezierPath+RoundedRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D3A6EF2EA3F49F001EF4F6 /* NSBezierPath+RoundedRect.swift */; };
|
||||
@@ -122,7 +140,12 @@
|
||||
/* 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 /* Apk+Manifest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Apk+Manifest.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; };
|
||||
543899072EDCA223007C02FC /* Apk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Apk.swift; sourceTree = "<group>"; };
|
||||
5438990A2EDCA27F007C02FC /* Apk+Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Apk+Icon.swift"; sourceTree = "<group>"; };
|
||||
543FE5732EB3BB5E0059F98B /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = "<group>"; };
|
||||
543FE5752EB3BC740059F98B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
54442BF42E378B71008A870E /* QLAppBundle (debug).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "QLAppBundle (debug).app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -130,11 +153,9 @@
|
||||
54442C222E378BAF008A870E /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; };
|
||||
54442C6A2E378BDD008A870E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
54442C6C2E378BDD008A870E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
54442C6E2E378BDD008A870E /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
|
||||
54442C732E378BE0008A870E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
54442C742E378BE0008A870E /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = "<group>"; };
|
||||
54442C752E378BE0008A870E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PreviewViewController.xib; sourceTree = "<group>"; };
|
||||
54442C772E378BE0008A870E /* QLPreview.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QLPreview.entitlements; sourceTree = "<group>"; };
|
||||
544AF3682EB6AAC0006837F2 /* AssetCarReader.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AssetCarReader.xcconfig; sourceTree = "<group>"; };
|
||||
54581FCF2EB29A0B0043A0B3 /* QLAppBundle Thumbnail Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "QLAppBundle Thumbnail Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54581FD02EB29A0B0043A0B3 /* QuickLookThumbnailing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookThumbnailing.framework; path = System/Library/Frameworks/QuickLookThumbnailing.framework; sourceTree = SDKROOT; };
|
||||
@@ -151,11 +172,18 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
54AE5BFB2EB3DB1000B4CFC7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
54AE5BFC2EB3DB1000B4CFC7 /* QLThumbnail.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QLThumbnail.entitlements; sourceTree = "<group>"; };
|
||||
54AE5BFD2EB3DB1000B4CFC7 /* ThumbnailProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailProvider.swift; sourceTree = "<group>"; };
|
||||
54B6FFEC2EB6A847007397C0 /* AssetCarReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCarReader.swift; sourceTree = "<group>"; };
|
||||
54CCF59D2EDC9A6800D766F9 /* AndroidSdkMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AndroidSdkMap.swift; sourceTree = "<group>"; };
|
||||
54D3A6EB2EA31B52001EF4F6 /* AppCategories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCategories.swift; sourceTree = "<group>"; };
|
||||
54D3A6ED2EA39CC6001EF4F6 /* AppIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = "<group>"; };
|
||||
54D3A6EF2EA3F49F001EF4F6 /* NSBezierPath+RoundedRect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSBezierPath+RoundedRect.swift"; sourceTree = "<group>"; };
|
||||
@@ -187,6 +215,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 */,
|
||||
@@ -197,6 +226,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 */,
|
||||
@@ -209,25 +239,70 @@
|
||||
541051562E37AFC10083670B /* src */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5405CF5D2EA1199B00613856 /* MetaInfo.swift */,
|
||||
54D3A6EB2EA31B52001EF4F6 /* AppCategories.swift */,
|
||||
54D3A6ED2EA39CC6001EF4F6 /* AppIcon.swift */,
|
||||
5469E11C2EA5930C00D46CE7 /* Entitlements.swift */,
|
||||
547F52DC2EB2C15D002B6D5F /* ExpirationStatus.swift */,
|
||||
547F52E62EB2C41C002B6D5F /* PreviewGenerator.swift */,
|
||||
547F52EC2EB2C822002B6D5F /* Preview+AppInfo.swift */,
|
||||
547F52EE2EB2C8E8002B6D5F /* Preview+Provisioning.swift */,
|
||||
547F52F32EB2CA05002B6D5F /* Preview+Entitlements.swift */,
|
||||
547F52E32EB2C3D8002B6D5F /* Preview+iTunesPurchase.swift */,
|
||||
547F52E92EB2C672002B6D5F /* Preview+FileInfo.swift */,
|
||||
547F52F62EB2CAC7002B6D5F /* Preview+Footer.swift */,
|
||||
5405CF642EA1376B00613856 /* Zip.swift */,
|
||||
54D3A6EF2EA3F49F001EF4F6 /* NSBezierPath+RoundedRect.swift */,
|
||||
547F52F82EB2CBAB002B6D5F /* Date+Format.swift */,
|
||||
543899122EDD0F38007C02FC /* Common */,
|
||||
543899112EDD0EE2007C02FC /* Data - Android */,
|
||||
543899102EDD0ED1007C02FC /* Data - Apple */,
|
||||
5438990F2EDD0EA1007C02FC /* Preview */,
|
||||
);
|
||||
path = src;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5438990F2EDD0EA1007C02FC /* Preview */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5412DECF2EBC283000F9040D /* RuntimeError.swift */,
|
||||
547F52DC2EB2C15D002B6D5F /* ExpirationStatus.swift */,
|
||||
547F52F82EB2CBAB002B6D5F /* Date+Format.swift */,
|
||||
547F52E62EB2C41C002B6D5F /* PreviewGenerator.swift */,
|
||||
547F52EC2EB2C822002B6D5F /* Preview+AppInfo.swift */,
|
||||
5412DECD2EBC168600F9040D /* Preview+ArchiveInfo.swift */,
|
||||
547F52E32EB2C3D8002B6D5F /* Preview+iTunesPurchase.swift */,
|
||||
549E3BA32EBC021500ADFF56 /* Preview+TransportSecurity.swift */,
|
||||
547F52F32EB2CA05002B6D5F /* Preview+Entitlements.swift */,
|
||||
547F52EE2EB2C8E8002B6D5F /* Preview+Provisioning.swift */,
|
||||
547F52E92EB2C672002B6D5F /* Preview+FileInfo.swift */,
|
||||
547F52F62EB2CAC7002B6D5F /* Preview+Footer.swift */,
|
||||
);
|
||||
path = Preview;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
543899102EDD0ED1007C02FC /* Data - Apple */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54D3A6EB2EA31B52001EF4F6 /* AppCategories.swift */,
|
||||
54993B892EDBA596008B656D /* Plist+Icon.swift */,
|
||||
54993B872EDB9A9B008B656D /* Plist+Info.swift */,
|
||||
54993B922EDBB419008B656D /* Plist+iTunesMetadata.swift */,
|
||||
54993B942EDBC813008B656D /* Plist+MobileProvision.swift */,
|
||||
54993B962EDC7C61008B656D /* Provisioning.swift */,
|
||||
5469E11C2EA5930C00D46CE7 /* Entitlements.swift */,
|
||||
);
|
||||
path = "Data - Apple";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
543899112EDD0EE2007C02FC /* Data - Android */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54CCF59D2EDC9A6800D766F9 /* AndroidSdkMap.swift */,
|
||||
543899072EDCA223007C02FC /* Apk.swift */,
|
||||
5438990A2EDCA27F007C02FC /* Apk+Icon.swift */,
|
||||
540B77D82ED79BB2009E030C /* Apk+Manifest.swift */,
|
||||
);
|
||||
path = "Data - Android";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
543899122EDD0F38007C02FC /* Common */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5405CF5D2EA1199B00613856 /* MetaInfo.swift */,
|
||||
54D3A6ED2EA39CC6001EF4F6 /* AppIcon.swift */,
|
||||
549E3BA02EBAE7D300ADFF56 /* URL+File.swift */,
|
||||
54D3A6EF2EA3F49F001EF4F6 /* NSBezierPath+RoundedRect.swift */,
|
||||
5405CF642EA1376B00613856 /* Zip.swift */,
|
||||
);
|
||||
path = Common;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54442BEB2E378B71008A870E = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -272,7 +347,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
543FE5752EB3BC740059F98B /* Info.plist */,
|
||||
54442C6E2E378BDD008A870E /* App.entitlements */,
|
||||
54442C6A2E378BDD008A870E /* AppDelegate.swift */,
|
||||
54442C6D2E378BDD008A870E /* MainMenu.xib */,
|
||||
);
|
||||
@@ -283,7 +357,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54442C732E378BE0008A870E /* Info.plist */,
|
||||
54442C772E378BE0008A870E /* QLPreview.entitlements */,
|
||||
54442C742E378BE0008A870E /* PreviewViewController.swift */,
|
||||
54442C762E378BE0008A870E /* PreviewViewController.xib */,
|
||||
);
|
||||
@@ -294,7 +367,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54AE5BFB2EB3DB1000B4CFC7 /* Info.plist */,
|
||||
54AE5BFC2EB3DB1000B4CFC7 /* QLThumbnail.entitlements */,
|
||||
54AE5BFD2EB3DB1000B4CFC7 /* ThumbnailProvider.swift */,
|
||||
);
|
||||
path = QLThumbnail;
|
||||
@@ -400,6 +472,7 @@
|
||||
);
|
||||
name = "QL Preview";
|
||||
packageProductDependencies = (
|
||||
540B77DB2ED79CC1009E030C /* AndroidXML */,
|
||||
);
|
||||
productName = QLPreview;
|
||||
productReference = 54442C202E378BAF008A870E /* QLAppBundle Preview Extension.appex */;
|
||||
@@ -421,6 +494,7 @@
|
||||
);
|
||||
name = "QL Thumbnail";
|
||||
packageProductDependencies = (
|
||||
540B77DD2ED79CC8009E030C /* AndroidXML */,
|
||||
);
|
||||
productName = QLThumbnail;
|
||||
productReference = 54581FCF2EB29A0B0043A0B3 /* QLAppBundle Thumbnail Extension.appex */;
|
||||
@@ -434,7 +508,7 @@
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1640;
|
||||
LastUpgradeCheck = 1640;
|
||||
LastUpgradeCheck = 2600;
|
||||
TargetAttributes = {
|
||||
54442BF32E378B71008A870E = {
|
||||
CreatedOnToolsVersion = 16.4;
|
||||
@@ -456,6 +530,9 @@
|
||||
);
|
||||
mainGroup = 54442BEB2E378B71008A870E;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
54D891112ED7313100BF23C4 /* XCRemoteSwiftPackageReference "AndroidXML" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 54442BF52E378B71008A870E /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -474,7 +551,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
544AF3692EB6AAC0006837F2 /* AssetCarReader.xcconfig in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -529,22 +605,35 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
543899082EDCA223007C02FC /* Apk.swift in Sources */,
|
||||
549E3BA42EBC021500ADFF56 /* Preview+TransportSecurity.swift in Sources */,
|
||||
5412DECE2EBC168600F9040D /* Preview+ArchiveInfo.swift in Sources */,
|
||||
54D3A6F02EA3F49F001EF4F6 /* NSBezierPath+RoundedRect.swift in Sources */,
|
||||
547F52E42EB2C3D8002B6D5F /* Preview+iTunesPurchase.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 */,
|
||||
54442C792E378BE0008A870E /* PreviewViewController.swift in Sources */,
|
||||
547F52EF2EB2C8E8002B6D5F /* Preview+Provisioning.swift in Sources */,
|
||||
547F52DE2EB2C15D002B6D5F /* ExpirationStatus.swift in Sources */,
|
||||
54D3A6EE2EA39CC6001EF4F6 /* AppIcon.swift in Sources */,
|
||||
54993B952EDBC819008B656D /* Plist+MobileProvision.swift in Sources */,
|
||||
547F52E82EB2C41C002B6D5F /* PreviewGenerator.swift in Sources */,
|
||||
54993B882EDB9AA1008B656D /* Plist+Info.swift in Sources */,
|
||||
5438990B2EDCA27F007C02FC /* Apk+Icon.swift in Sources */,
|
||||
547F52EB2EB2C672002B6D5F /* Preview+FileInfo.swift in Sources */,
|
||||
54CCF59E2EDC9A6800D766F9 /* AndroidSdkMap.swift in Sources */,
|
||||
547F52ED2EB2C822002B6D5F /* Preview+AppInfo.swift in Sources */,
|
||||
549E3BA12EBAE7D300ADFF56 /* URL+File.swift in Sources */,
|
||||
547F52F42EB2CA05002B6D5F /* Preview+Entitlements.swift in Sources */,
|
||||
540B77D92ED79BBD009E030C /* Apk+Manifest.swift in Sources */,
|
||||
5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */,
|
||||
547F52F92EB2CBAB002B6D5F /* Date+Format.swift in Sources */,
|
||||
54D3A6EC2EA31B52001EF4F6 /* AppCategories.swift in Sources */,
|
||||
54993B932EDBB41D008B656D /* Plist+iTunesMetadata.swift in Sources */,
|
||||
5405CF652EA1376B00613856 /* Zip.swift in Sources */,
|
||||
5412DED02EBC283000F9040D /* RuntimeError.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -556,7 +645,11 @@
|
||||
547899722EB38F3D00F96B80 /* MetaInfo.swift in Sources */,
|
||||
547899732EB38F3D00F96B80 /* NSBezierPath+RoundedRect.swift in Sources */,
|
||||
54AE5BFF2EB3DB1000B4CFC7 /* ThumbnailProvider.swift in Sources */,
|
||||
543899092EDCA26D007C02FC /* Apk.swift in Sources */,
|
||||
549E3BA22EBAECD400ADFF56 /* URL+File.swift in Sources */,
|
||||
547899752EB38F3D00F96B80 /* AppIcon.swift in Sources */,
|
||||
54993B8B2EDBA75A008B656D /* Plist+Icon.swift in Sources */,
|
||||
5438990E2EDCB126007C02FC /* Apk+Icon.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -610,6 +703,7 @@
|
||||
baseConfigurationReference = 544AF3682EB6AAC0006837F2 /* AssetCarReader.xcconfig */;
|
||||
buildSettings = {
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
@@ -644,6 +738,7 @@
|
||||
baseConfigurationReference = 544AF3682EB6AAC0006837F2 /* AssetCarReader.xcconfig */;
|
||||
buildSettings = {
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
@@ -708,7 +803,6 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1655;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
@@ -734,6 +828,7 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
@@ -774,7 +869,6 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1655;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
@@ -794,6 +888,7 @@
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
};
|
||||
name = Release;
|
||||
@@ -801,13 +896,15 @@
|
||||
54442C022E378B71008A870E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = UY657LKNHJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
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;
|
||||
@@ -826,13 +923,15 @@
|
||||
54442C032E378B71008A870E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = UY657LKNHJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
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;
|
||||
@@ -850,10 +949,11 @@
|
||||
54442C322E378BAF008A870E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = QLPreview/QLPreview.entitlements;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = UY657LKNHJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QLPreview/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "$(PRODUCT_NAME) (debug)";
|
||||
@@ -875,10 +975,11 @@
|
||||
54442C332E378BAF008A870E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = QLPreview/QLPreview.entitlements;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = UY657LKNHJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QLPreview/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "$(PRODUCT_NAME)";
|
||||
@@ -900,10 +1001,11 @@
|
||||
54581FDB2EB29A0B0043A0B3 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = QLThumbnail/QLThumbnail.entitlements;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = UY657LKNHJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QLThumbnail/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "$(PRODUCT_NAME) (debug)";
|
||||
@@ -925,10 +1027,11 @@
|
||||
54581FDC2EB29A0B0043A0B3 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = QLThumbnail/QLThumbnail.entitlements;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = UY657LKNHJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = QLThumbnail/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "$(PRODUCT_NAME)";
|
||||
@@ -996,6 +1099,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
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1640"
|
||||
LastUpgradeVersion = "2600"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -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,12 +13,9 @@ class PreviewViewController: NSViewController, QLPreviewingController {
|
||||
|
||||
/// Load resource file either from user documents dir (if exists) or app bundle (default).
|
||||
func bundleFile(filename: String, ext: String) throws -> String {
|
||||
if let appSupport = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||||
let override = appSupport.appendingPathComponent(filename + "." + ext)
|
||||
if FileManager.default.fileExists(atPath: override.path) {
|
||||
return try String(contentsOfFile: override.path, encoding: .utf8)
|
||||
}
|
||||
// else: do NOT copy! Breaks on future updates
|
||||
if let userFile = URL.UserModDir?.appendingPathComponent(filename + "." + ext, isDirectory: false), userFile.exists() {
|
||||
return try String(contentsOf: userFile, encoding: .utf8)
|
||||
// else: do NOT copy! Breaks on future updates
|
||||
}
|
||||
// else, load bundle file
|
||||
let path = Bundle.main.url(forResource: filename, withExtension: ext)
|
||||
@@ -27,7 +24,8 @@ class PreviewViewController: NSViewController, QLPreviewingController {
|
||||
|
||||
func preparePreviewOfFile(at url: URL) async throws {
|
||||
let meta = MetaInfo(url)
|
||||
let html = PreviewGenerator(meta).generate(
|
||||
// throws an exception if appPlist not found. Thus allowing another QuickLook plugin to try
|
||||
let html = try PreviewGenerator(meta).generate(
|
||||
template: try bundleFile(filename: "template", ext: "html"),
|
||||
css: try bundleFile(filename: "style", ext: "css"),
|
||||
)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -9,10 +9,13 @@
|
||||
<key>QLSupportedContentTypes</key>
|
||||
<array>
|
||||
<string>com.apple.itunes.ipa</string>
|
||||
<string>com.apple.application-and-system-extension</string>
|
||||
<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>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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
|
||||
|
||||
16
README.md
16
README.md
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -36,10 +37,11 @@ Also, I've removed support for provisioning profiles (`.mobileprovision`, `.prov
|
||||
### Customize HTML / CSS
|
||||
|
||||
1. Right click on the app and select "Show Package Contents"
|
||||
2. Copy `Contents/Resources/template.html` (or `style.css`)
|
||||
3. Open `~/Library/Containers/de.relikd.QLAppBundle.Preview/Data/Documents/`
|
||||
4. Paste the previous file and modify it to your liking
|
||||
5. `QLAppBundle` will use the new file from now on
|
||||
2. Go to `PlugIns` and repeat "Show Package Contents" on the Preview extension.
|
||||
3. Copy `Contents/Resources/template.html` (or `style.css`)
|
||||
4. Open `~/Library/Containers/de.relikd.QLAppBundle.Preview/Data/Documents/`
|
||||
5. Paste the previous file and modify it to your liking
|
||||
6. `QLAppBundle` will use the new file from now on
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ body {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.hiddenDiv {
|
||||
.hidden, .not- {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,79 +2,100 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>__CSS__</style>
|
||||
<style>{{CSS}}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<h1>__QuickLookTitle__</h1>
|
||||
<div class="floatLeft icon"><img alt="App icon" src="data:image/png;base64,__AppIcon__"/></div>
|
||||
<h1>{{QuickLookTitle}}</h1>
|
||||
|
||||
<div class="app {{AppInfoHidden}}">
|
||||
<div class="floatLeft icon"><img alt="App icon" src="data:image/png;base64,{{AppIcon}}"/></div>
|
||||
<div class="floatLeft info">
|
||||
Name: <strong>__AppName__</strong><br />
|
||||
Version: __AppVersion__ (__AppBuildVer__)<br />
|
||||
BundleId: __AppId__<br />
|
||||
<div class="__AppExtensionTypeHidden__">
|
||||
Extension type: __AppExtensionType__<br />
|
||||
Name: <strong>{{AppName}}</strong><br />
|
||||
Version: {{AppVersion}} ({{AppBuildVer}})<br />
|
||||
BundleId: {{AppId}}<br />
|
||||
<div class="{{AppExtensionTypeHidden}}">
|
||||
Extension type: {{AppExtensionType}}<br />
|
||||
</div>
|
||||
DeviceFamily: __AppDeviceFamily__<br />
|
||||
SDK: __AppSDK__<br />
|
||||
Minimum OS Version: __AppMinOS__<br />
|
||||
DeviceFamily: {{AppDeviceFamily}}<br />
|
||||
SDK: {{AppSDK}}<br />
|
||||
Minimum OS Version: {{AppMinOS}}<br />
|
||||
</div>
|
||||
<br class="clear" />
|
||||
</div>
|
||||
|
||||
<div class="__iTunesHidden__">
|
||||
<div class="not-{{AppInfoHidden}}">
|
||||
Could not find any Info.plist
|
||||
</div>
|
||||
|
||||
<div class="{{ArchiveHidden}}">
|
||||
<h2>Archive Notes</h2>
|
||||
<pre>{{ArchiveComment}}</pre>
|
||||
</div>
|
||||
|
||||
<div class="{{iTunesHidden}}">
|
||||
<h2>iTunes Metadata</h2>
|
||||
iTunesId: __iTunesId__<br />
|
||||
Title: __iTunesName__<br />
|
||||
Genres: __iTunesGenres__<br />
|
||||
Released: __iTunesReleaseDate__<br />
|
||||
iTunesId: {{iTunesId}}<br />
|
||||
Title: {{iTunesName}}<br />
|
||||
Genres: {{iTunesGenres}}<br />
|
||||
Released: {{iTunesReleaseDate}}<br />
|
||||
<br />
|
||||
AppleId: __iTunesAppleId__<br />
|
||||
Purchased: __iTunesPurchaseDate__<br />
|
||||
Price: __iTunesPrice__<br />
|
||||
AppleId: {{iTunesAppleId}}<br />
|
||||
Purchased: {{iTunesPurchaseDate}}<br />
|
||||
Price: {{iTunesPrice}}<br />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="{{TransportSecurityHidden}}">
|
||||
<h2>App Transport Security</h2>
|
||||
__AppTransportSecurity__
|
||||
{{TransportSecurityDict}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="{{EntitlementsHidden}}">
|
||||
<h2>Entitlements</h2>
|
||||
<div class="warning __EntitlementsWarningHidden__">
|
||||
<div class="warning {{EntitlementsWarningHidden}}">
|
||||
<strong>Entitlements extraction failed.</strong>
|
||||
</div>
|
||||
__EntitlementsDict__
|
||||
{{EntitlementsDict}}
|
||||
</div>
|
||||
|
||||
<div class="__ProvisionHidden__">
|
||||
<div class="{{ProvisionHidden}}">
|
||||
<h2>Provisioning</h2>
|
||||
Profile name: <strong>__ProvisionProfileName__</strong><br />
|
||||
Profile UUID: __ProvisionProfileId__<br />
|
||||
Profile Type: __ProvisionProfilePlatform__ __ProvisionProfileType__<br />
|
||||
Team: __ProvisionTeamName__ (__ProvisionTeamIds__)<br />
|
||||
Creation date: __ProvisionCreateDate__<br />
|
||||
Expiration Date: <strong><span class="__ProvisionExpireStatus__">__ProvisionExpireDate__</span></strong><br />
|
||||
</div>
|
||||
|
||||
<div class="__ProvisionHidden__">
|
||||
Profile name: <strong>{{ProvisionProfileName}}</strong><br />
|
||||
Profile UUID: {{ProvisionProfileId}}<br />
|
||||
Profile Type: {{ProvisionProfilePlatform}} {{ProvisionProfileType}}<br />
|
||||
Team: {{ProvisionTeamName}} ({{ProvisionTeamIds}})<br />
|
||||
Creation date: {{ProvisionCreateDate}}<br />
|
||||
Expiration Date: <strong><span class="{{ProvisionExpireStatus}}">{{ProvisionExpireDate}}</span></strong><br />
|
||||
|
||||
<h2>Developer Certificates</h2>
|
||||
__ProvisionDevelopCertificates__
|
||||
{{ProvisionDevelopCertificates}}
|
||||
|
||||
<h2>Devices ({{ProvisionDeviceCount}})</h2>
|
||||
{{ProvisionDeviceIds}}
|
||||
</div>
|
||||
|
||||
<div class="__ProvisionHidden__">
|
||||
<h2>Devices (__ProvisionDeviceCount__)</h2>
|
||||
__ProvisionDeviceIds__
|
||||
<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 />
|
||||
__FileSize__, Modified __FileModified__<br />
|
||||
{{FileName}}<br />
|
||||
{{FileSize}}, Modified {{FileModified}}<br />
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>__SrcAppName__ v__SrcVersion__ (__SrcBuildVer__) (Github: <a href="__SrcLinkUrl__">__SrcLinkName__</a>)</p>
|
||||
<p>{{SrcAppName}} v{{SrcVersion}} ({{SrcBuildVer}}) (Github: <a href="{{SrcLinkUrl}}">{{SrcLinkName}}</a>)</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
screenshot2.png
Normal file
BIN
screenshot2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
@@ -13,24 +13,44 @@ 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.readPlist_Icon()?.filenames)
|
||||
case .APK:
|
||||
extractImage(from: meta.readApk_Icon())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract image from Android app bundle.
|
||||
func extractImage(from apkIcon: Apk_Icon?) -> NSImage {
|
||||
if let data = apkIcon?.data, 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 {
|
||||
func extractImage(from plistIcons: [String]?) -> NSImage {
|
||||
// no need to unwrap the plist, and most .ipa should include the Artwork anyway
|
||||
if meta.type == .IPA {
|
||||
if let data = meta.zipFile!.unzipFile("iTunesArtwork") {
|
||||
os_log(.debug, log: log, "[icon] using iTunesArtwork.")
|
||||
return NSImage(data: data)!
|
||||
}
|
||||
// else, fallthrough
|
||||
}
|
||||
|
||||
// Extract image name from app plist
|
||||
var plistImgNames = iconNamesFromPlist(appPlist)
|
||||
var plistImgNames = plistIcons ?? []
|
||||
os_log(.debug, log: log, "[icon] icon names in plist: %{public}@", plistImgNames)
|
||||
|
||||
// If no previous filename works (or empty), try default icon names
|
||||
plistImgNames.append("Icon")
|
||||
plistImgNames.append("icon")
|
||||
plistImgNames.append("AppIcon")
|
||||
|
||||
// First, try if an image file with that name exists.
|
||||
if let actualName = expandImageName(plistImgNames) {
|
||||
@@ -48,13 +68,18 @@ 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)!
|
||||
}
|
||||
|
||||
/// Extract an image from `Assets.car`
|
||||
func imageFromAssetsCar(_ imageName: String) -> NSImage? {
|
||||
guard let data = meta.readPayloadFile("Assets.car") else {
|
||||
guard let data = meta.readPayloadFile("Assets.car", osxSubdir: "Resources") else {
|
||||
return nil
|
||||
}
|
||||
return CarReader(data)?.imageFromAssetsCar(imageName)
|
||||
@@ -65,45 +90,14 @@ struct AppIcon {
|
||||
// MARK: - Plist
|
||||
|
||||
extension AppIcon {
|
||||
/// Parse app plist to find the bundle icon filename.
|
||||
/// @param appPlist If `nil`, will load plist on the fly (used for thumbnail)
|
||||
/// @return Filenames which do not necessarily exist on filesystem. This may include `@2x` and/or no file extension.
|
||||
private func iconNamesFromPlist(_ appPlist: PlistDict) -> [String] {
|
||||
// Check for CFBundleIcons (since 5.0)
|
||||
if let icons = unpackNameListFromPlistDict(appPlist["CFBundleIcons"]), !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// iPad-only apps
|
||||
if let icons = unpackNameListFromPlistDict(appPlist["CFBundleIcons~ipad"]), !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// Check for CFBundleIconFiles (since 3.2)
|
||||
if let icons = appPlist["CFBundleIconFiles"] as? [String], !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// key found on iTunesU app
|
||||
if let icons = appPlist["Icon files"] as? [String], !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// Check for CFBundleIconFile (legacy, before 3.2)
|
||||
if let icon = appPlist["CFBundleIconFile"] as? String { // may be nil
|
||||
return [icon]
|
||||
}
|
||||
return [] // [self sortedByResolution:icons];
|
||||
}
|
||||
|
||||
/// Given a filename, search Bundle or Filesystem for files that match. Select the filename with the highest resolution.
|
||||
private func expandImageName(_ iconList: [String]) -> String? {
|
||||
var matches: [String] = []
|
||||
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)
|
||||
}
|
||||
@@ -113,11 +107,13 @@ extension AppIcon {
|
||||
}
|
||||
}
|
||||
|
||||
case .APK:
|
||||
return nil // handled in `extractImage()`
|
||||
|
||||
case .Archive, .Extension:
|
||||
let basePath = meta.effectiveUrl ?? meta.url
|
||||
for iconPath in iconList {
|
||||
let fileName = iconPath.components(separatedBy: "/").last!
|
||||
let parentDir = basePath.appendingPathComponent(iconPath, isDirectory: false).deletingLastPathComponent().path
|
||||
let parentDir = meta.effectiveUrl("Resources", iconPath).parentDir().path
|
||||
guard let files = try? FileManager.default.contentsOfDirectory(atPath: parentDir) else {
|
||||
continue
|
||||
}
|
||||
@@ -138,21 +134,6 @@ extension AppIcon {
|
||||
}
|
||||
return matches.isEmpty ? nil : sortedByResolution(matches).first
|
||||
}
|
||||
|
||||
/// Deep select icons from plist key `CFBundleIcons` and `CFBundleIcons~ipad`
|
||||
private func unpackNameListFromPlistDict(_ bundleDict: Any?) -> [String]? {
|
||||
if let bundleDict = bundleDict as? PlistDict {
|
||||
if let primaryDict = bundleDict["CFBundlePrimaryIcon"] as? PlistDict {
|
||||
if let icons = primaryDict["CFBundleIconFiles"] as? [String] {
|
||||
return icons
|
||||
}
|
||||
if let name = primaryDict["CFBundleIconName"] as? String { // key found on a .tipa file
|
||||
return [name]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// @return lower index means higher resolution.
|
||||
private func resolutionIndex(_ iconName: String) -> Int {
|
||||
@@ -218,7 +199,6 @@ extension NSImage {
|
||||
|
||||
/// Convert image to PNG and encode with base64 to be embeded in html output.
|
||||
func asBase64() -> String {
|
||||
// appIcon = [self roundCorners:appIcon];
|
||||
let imageData = tiffRepresentation!
|
||||
let imageRep = NSBitmapImageRep(data: imageData)!
|
||||
let imageDataPNG = imageRep.representation(using: .png, properties: [:])!
|
||||
134
src/Common/MetaInfo.swift
Normal file
134
src/Common/MetaInfo.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
import Foundation
|
||||
import os // OSLog
|
||||
|
||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "MetaInfo")
|
||||
|
||||
typealias PlistDict = [String: Any] // basically an untyped Dict
|
||||
|
||||
|
||||
// Init QuickLook Type
|
||||
enum FileType {
|
||||
case IPA
|
||||
case Archive
|
||||
case Extension
|
||||
case APK
|
||||
}
|
||||
|
||||
struct MetaInfo {
|
||||
let UTI: String
|
||||
let url: URL
|
||||
private let effectiveUrl: URL // if set, will point to the app inside of an archive
|
||||
|
||||
let type: FileType
|
||||
let zipFile: ZipFile? // only set for zipped file types
|
||||
let isOSX: Bool
|
||||
|
||||
/// 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"
|
||||
|
||||
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" /* tipa */:
|
||||
self.type = FileType.IPA
|
||||
zipFile = ZipFile(self.url.path)
|
||||
case "com.apple.xcode.archive":
|
||||
self.type = FileType.Archive
|
||||
let productsDir = url.appendingPathComponent("Products", isDirectory: true)
|
||||
if productsDir.exists(), let bundleDir = recursiveSearchInfoPlist(productsDir) {
|
||||
isOSX = bundleDir.appendingPathComponent("MacOS").exists() && bundleDir.lastPathComponent == "Contents"
|
||||
effective = bundleDir
|
||||
} else {
|
||||
effective = productsDir // this is wrong but dont use `url` either because that will find the `Info.plist` of the archive itself
|
||||
}
|
||||
case "com.apple.application-and-system-extension":
|
||||
self.type = FileType.Extension
|
||||
if let bundleDir = recursiveSearchInfoPlist(url) {
|
||||
isOSX = bundleDir.appendingPathComponent("MacOS").exists() && bundleDir.lastPathComponent == "Contents"
|
||||
effective = bundleDir
|
||||
}
|
||||
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()
|
||||
}
|
||||
self.isOSX = isOSX
|
||||
self.zipFile = zipFile
|
||||
self.effectiveUrl = effective ?? url
|
||||
}
|
||||
|
||||
/// Evaluate path with `osxSubdir` and `filename`
|
||||
func effectiveUrl(_ osxSubdir: String?, _ filename: String) -> URL {
|
||||
switch self.type {
|
||||
case .IPA, .APK:
|
||||
return effectiveUrl
|
||||
case .Archive, .Extension:
|
||||
if isOSX, let osxSubdir {
|
||||
return effectiveUrl
|
||||
.appendingPathComponent(osxSubdir, isDirectory: true)
|
||||
.appendingPathComponent(filename, isDirectory: false)
|
||||
}
|
||||
return effectiveUrl.appendingPathComponent(filename, isDirectory: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a file from bundle into memory. Either by file path or via unzip.
|
||||
func readPayloadFile(_ filename: String, osxSubdir: String?) -> Data? {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Plist
|
||||
|
||||
extension Data {
|
||||
/// Helper for optional chaining.
|
||||
func asPlistOrNil() -> PlistDict? {
|
||||
if self.isEmpty {
|
||||
return nil
|
||||
}
|
||||
// var format: PropertyListSerialization.PropertyListFormat = .xml
|
||||
do {
|
||||
return try PropertyListSerialization.propertyList(from: self, format: nil) as? PlistDict
|
||||
} catch {
|
||||
os_log(.error, log: log, "ERROR reading plist %{public}@", error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - helper methods
|
||||
|
||||
/// breadth-first search for `Info.plist`
|
||||
private func recursiveSearchInfoPlist(_ url: URL) -> URL? {
|
||||
var queue: [URL] = [url]
|
||||
while !queue.isEmpty {
|
||||
let current = queue.removeLast()
|
||||
if current.pathExtension == "framework" {
|
||||
continue // do not evaluate bundled frameworks
|
||||
}
|
||||
if let subfiles = try? FileManager.default.contentsOfDirectory(at: current, includingPropertiesForKeys: []) {
|
||||
for fname in subfiles {
|
||||
if fname.lastPathComponent == "Info.plist" {
|
||||
return fname.parentDir()
|
||||
}
|
||||
}
|
||||
queue.append(contentsOf: subfiles)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
17
src/Common/URL+File.swift
Normal file
17
src/Common/URL+File.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
/// Folder where user can mofifications to html template
|
||||
static let UserModDir: URL? =
|
||||
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
||||
|
||||
/// Returns `true` if file or folder exists.
|
||||
@inlinable func exists() -> Bool {
|
||||
FileManager.default.fileExists(atPath: self.path)
|
||||
}
|
||||
|
||||
/// Returns URL by deleting last path component
|
||||
@inlinable func parentDir() -> URL {
|
||||
self.deletingLastPathComponent()
|
||||
}
|
||||
}
|
||||
@@ -224,9 +224,9 @@ private func listZip(_ path: String) -> [ZipEntry] {
|
||||
}
|
||||
|
||||
guard let endRecord = findCentralDirectory(fp), endRecord.sizeOfCentralDirectory > 0 else {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
return listDirectoryEntries(fp, endRecord);
|
||||
return listDirectoryEntries(fp, endRecord)
|
||||
}
|
||||
|
||||
/// Find signature for central directory.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
45
src/Data - Android/AndroidSdkMap.swift
Normal file
45
src/Data - Android/AndroidSdkMap.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
// see https://developer.android.com/guide/topics/manifest/uses-sdk-element#api-level-table
|
||||
|
||||
let AndroidSdkMap: [Int: 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",
|
||||
]
|
||||
74
src/Data - Android/Apk+Icon.swift
Normal file
74
src/Data - Android/Apk+Icon.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
import Foundation
|
||||
import AndroidXML
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `AndroidManifest.xml` but only extract `appIcon`.
|
||||
func readApk_Icon() -> Apk_Icon? {
|
||||
assert(type == .APK)
|
||||
var apk = Apk(self)
|
||||
return Apk_Icon(&apk)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Apk_Icon
|
||||
|
||||
/// Representation of `AndroidManifest.xml` (containing only the icon extractor).
|
||||
/// Seperate from main class because everything else is not needed for `ThumbnailProvider`
|
||||
struct Apk_Icon {
|
||||
let path: String
|
||||
let data: Data
|
||||
|
||||
init?(_ apk: inout Apk, iconRef: String? = nil) {
|
||||
if apk.isApkm, let iconData = apk.mainZip.unzipFile("icon.png") {
|
||||
path = "icon.png"
|
||||
data = iconData
|
||||
return
|
||||
}
|
||||
|
||||
guard let manifest = apk.manifest else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ref = iconRef
|
||||
// no need to parse xml if reference already supplied
|
||||
if ref == nil {
|
||||
if let xml = try? AndroidXML.init(data: manifest) {
|
||||
let parser = xml.parseXml()
|
||||
try? parser.iterElements({ startTag, attributes in
|
||||
if startTag == "application" {
|
||||
ref = try? attributes.get("android:icon")?.resolve(parser.stringPool)
|
||||
}
|
||||
}) {_ in}
|
||||
} else {
|
||||
// fallback to xml-string parser
|
||||
ref = ApkXmlIconParser().run(manifest)
|
||||
}
|
||||
}
|
||||
|
||||
guard let img = apk.resolveIcon(&ref) else {
|
||||
return nil
|
||||
}
|
||||
path = ref!
|
||||
data = img
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/Data - Android/Apk+Manifest.swift
Normal file
123
src/Data - Android/Apk+Manifest.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
import Foundation
|
||||
import AndroidXML
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `AndroidManifest.xml` and parse its content
|
||||
func readApk_Manifest() -> Apk_Manifest? {
|
||||
assert(type == .APK)
|
||||
var apk = Apk(self)
|
||||
return Apk_Manifest.from(&apk)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Apk_Manifest
|
||||
|
||||
/// Representation of `AndroidManifest.xml`.
|
||||
/// See: <https://developer.android.com/guide/topics/manifest/manifest-element>
|
||||
struct Apk_Manifest {
|
||||
var packageId: String? = nil
|
||||
var appName: String? = nil
|
||||
var icon: Apk_Icon? = nil
|
||||
/// Computed property
|
||||
var appIconData: Data? = nil
|
||||
var versionName: String? = nil
|
||||
var versionCode: String? = nil
|
||||
var sdkVerMin: Int? = nil
|
||||
var sdkVerTarget: Int? = nil
|
||||
|
||||
var featuresRequired: [String] = []
|
||||
var featuresOptional: [String] = []
|
||||
var permissions: [String] = []
|
||||
|
||||
static func from(_ apk: inout Apk) -> Self? {
|
||||
guard let manifest = apk.manifest else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let storage = ApkXmlManifestParser()
|
||||
if let xml = try? AndroidXML.init(data: manifest) {
|
||||
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: manifest)
|
||||
parser.delegate = storage
|
||||
parser.parse()
|
||||
}
|
||||
|
||||
var rv = storage.result
|
||||
apk.resolveName(&rv.appName)
|
||||
rv.icon = Apk_Icon(&apk, iconRef: storage.iconRef)
|
||||
return rv
|
||||
}
|
||||
}
|
||||
|
||||
// keep in sync with `ApkXmlManifestParser` below
|
||||
private let ALLOWED_TAGS = [
|
||||
"manifest",
|
||||
"application",
|
||||
"uses-feature",
|
||||
"uses-permission",
|
||||
"uses-permission-sdk-23",
|
||||
"uses-sdk",
|
||||
]
|
||||
|
||||
/// Wrapper to use same code for binary-xml and string-xml parsing
|
||||
private class ApkXmlManifestParser: NSObject, XMLParserDelegate {
|
||||
private var _scope: [String] = []
|
||||
var result = Apk_Manifest()
|
||||
var iconRef: String? = nil
|
||||
|
||||
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
|
||||
iconRef = 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 = Int(attrs["android:minSdkVersion"] ?? "1") // "21"
|
||||
result.sdkVerTarget = Int(attrs["android:targetSdkVersion"] ?? "-1") // "35"
|
||||
}
|
||||
default: break // ignore
|
||||
}
|
||||
_scope.append(elementName)
|
||||
}
|
||||
|
||||
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
|
||||
_scope.removeLast()
|
||||
}
|
||||
}
|
||||
130
src/Data - Android/Apk.swift
Normal file
130
src/Data - Android/Apk.swift
Normal file
@@ -0,0 +1,130 @@
|
||||
import Foundation
|
||||
import AndroidXML
|
||||
|
||||
/// Data structure for processing the content of `.apk` files.
|
||||
struct Apk {
|
||||
let isApkm: Bool
|
||||
let mainZip: ZipFile
|
||||
|
||||
init(_ meta: MetaInfo) {
|
||||
isApkm = meta.url.pathExtension.lowercased() == "apkm"
|
||||
mainZip = meta.zipFile!
|
||||
}
|
||||
|
||||
/// Unzip `AndroidManifest.xml` (once). Data is cached until deconstructor.
|
||||
lazy var manifest: Data? = { effectiveZip?.unzipFile("AndroidManifest.xml") }()
|
||||
|
||||
/// Select zip-file depending on `.apk` or `.apkm` extension
|
||||
private lazy var effectiveZip: ZipFile? = { isApkm ? nestedZip : mainZip }()
|
||||
|
||||
/// `.apkm` may contain multiple `.apk` files. (plus "icon.png" and "info.json" files)
|
||||
private lazy var nestedZip: ZipFile? = {
|
||||
if isApkm, let pth = try? mainZip.unzipFileToTempDir("base.apk") {
|
||||
return ZipFile(pth)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
/// Shared instance for resolving resources
|
||||
private lazy var resourceParser: Tbl_Parser? = {
|
||||
guard let data = effectiveZip?.unzipFile("resources.arsc"),
|
||||
let xml = try? AndroidXML.init(data: data), xml.type == .Table else {
|
||||
return nil
|
||||
}
|
||||
return xml.parseTable()
|
||||
}()
|
||||
|
||||
/// Lookup app bundle name / label
|
||||
mutating func resolveName(_ name: inout String?) {
|
||||
if let val = name, let ref = try? TblTableRef(val), let parser = resourceParser {
|
||||
name = parser.getName(ref)
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup image path and image data
|
||||
mutating func resolveIcon(_ iconRef: inout String?) -> Data? {
|
||||
if let val = iconRef, let ref = try? TblTableRef(val), let parser = resourceParser {
|
||||
if let iconPath = parser.getIconDirect(ref) ?? parser.getIconIndirect(ref) {
|
||||
iconRef = iconPath
|
||||
return effectiveZip?.unzipFile(iconPath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import Foundation
|
||||
|
||||
/*
|
||||
#!/usr/bin/env python3
|
||||
# download: https://itunes.apple.com/WebObjects/MZStoreServices.woa/ws/genres
|
||||
@@ -67,6 +67,8 @@ struct Entitlements {
|
||||
|
||||
// MARK: - SecCode in-memory reader
|
||||
|
||||
// Same as system call:
|
||||
// `codesign -d ./binary --entitlements - --xml` or: `codesign -d ./binary --entitlements :-`
|
||||
/// use in-memory `SecCode` for entitlement extraction
|
||||
private func getSecCodeEntitlements() -> PlistDict? {
|
||||
let url = URL(fileURLWithPath: self.binaryPath)
|
||||
@@ -84,13 +86,13 @@ struct Entitlements {
|
||||
|
||||
// if 'entitlements-dict' key exists, use that one
|
||||
os_log(.debug, log: log, "[entitlements] read SecCode 'entitlements-dict' key")
|
||||
if let plist = requirementInfo[kSecCodeInfoEntitlementsDict as String] as? PlistDict {
|
||||
if let plist = requirementInfo[kSecCodeInfoEntitlementsDict as String] as? PlistDict, !plist.isEmpty {
|
||||
return plist
|
||||
}
|
||||
|
||||
// else, fallback to parse data from 'entitlements' key
|
||||
os_log(.debug, log: log, "[entitlements] read SecCode 'entitlements' key")
|
||||
guard let data = requirementInfo[kSecCodeInfoEntitlements as String] as? Data else {
|
||||
guard let data = requirementInfo[kSecCodeInfoEntitlements as String] as? Data, !data.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -107,7 +109,10 @@ struct Entitlements {
|
||||
os_log(.error, log: log, "[entitlements] unpack error for FADE7171 size %lu != %lu", data.count, size)
|
||||
// but try anyway
|
||||
}
|
||||
return data.subdata(in: 8..<data.count).asPlistOrNil()
|
||||
guard let rv = data.subdata(in: 8..<data.count).asPlistOrNil(), !rv.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
|
||||
65
src/Data - Apple/Plist+Icon.swift
Normal file
65
src/Data - Apple/Plist+Icon.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `Info.plist`. (used for `ThumbnailProvider`)
|
||||
func readPlist_Icon() -> Plist_Icon? {
|
||||
if let x = self.readPayloadFile("Info.plist", osxSubdir: nil)?.asPlistOrNil() {
|
||||
return Plist_Icon(x)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Plist_Icon
|
||||
|
||||
/// Representation of `Info.plist` (containing only the icon extractor).
|
||||
/// Seperate from main class because everything else is not needed for `ThumbnailProvider`
|
||||
struct Plist_Icon {
|
||||
let filenames: [String]
|
||||
|
||||
init(_ plist: PlistDict) {
|
||||
filenames = parseIconNames(plist)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find icon filenames.
|
||||
/// @return Filenames which do not necessarily exist on filesystem. This may include `@2x` and/or no file extension.
|
||||
private func parseIconNames(_ plist: PlistDict) -> [String] {
|
||||
// Check for CFBundleIcons (since 5.0)
|
||||
if let icons = unpackNameList(plist["CFBundleIcons"]) {
|
||||
return icons
|
||||
}
|
||||
// iPad-only apps
|
||||
if let icons = unpackNameList(plist["CFBundleIcons~ipad"]) {
|
||||
return icons
|
||||
}
|
||||
// Check for CFBundleIconFiles (since 3.2)
|
||||
if let icons = plist["CFBundleIconFiles"] as? [String], !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// key found on iTunesU app
|
||||
if let icons = plist["Icon files"] as? [String], !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// Check for CFBundleIconFile (legacy, before 3.2)
|
||||
if let icon = plist["CFBundleIconFile"] as? String { // may be nil
|
||||
return [icon]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/// Deep select icons from plist key `CFBundleIcons` and `CFBundleIcons~ipad`
|
||||
/// @return Guarantees a non-empty array (or `nil`)
|
||||
private func unpackNameList(_ bundleDict: Any?) -> [String]? {
|
||||
if let bundleDict = bundleDict as? PlistDict {
|
||||
if let primaryDict = bundleDict["CFBundlePrimaryIcon"] as? PlistDict {
|
||||
if let icons = primaryDict["CFBundleIconFiles"] as? [String], !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
if let name = primaryDict["CFBundleIconName"] as? String { // key found on a .tipa file
|
||||
return [name]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
70
src/Data - Apple/Plist+Info.swift
Normal file
70
src/Data - Apple/Plist+Info.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
import Foundation
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `Info.plist`. (used for `PreviewProvider`)
|
||||
func readPlist_Info() -> Plist_Info? {
|
||||
if let x = self.readPayloadFile("Info.plist", osxSubdir: nil)?.asPlistOrNil() {
|
||||
return Plist_Info(x, isOSX: isOSX)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Plist_Info
|
||||
|
||||
/// Representation of `Info.plist` of an `.ipa` bundle
|
||||
struct Plist_Info {
|
||||
let bundleId: String?
|
||||
let name: String?
|
||||
let version: String?
|
||||
let buildVersion: String?
|
||||
|
||||
let exePath: String?
|
||||
let sdkVersion: String?
|
||||
let minOS: String?
|
||||
let extensionType: String?
|
||||
|
||||
let icons: [String]
|
||||
let deviceFamily: [String]
|
||||
let transportSecurity: PlistDict?
|
||||
|
||||
init(_ plist: PlistDict, isOSX: Bool) {
|
||||
bundleId = plist["CFBundleIdentifier"] as? String
|
||||
name = plist["CFBundleDisplayName"] as? String ?? plist["CFBundleName"] as? String
|
||||
version = plist["CFBundleShortVersionString"] as? String
|
||||
buildVersion = plist["CFBundleVersion"] as? String
|
||||
exePath = plist["CFBundleExecutable"] as? String
|
||||
sdkVersion = plist["DTSDKName"] as? String
|
||||
minOS = plist[isOSX ? "LSMinimumSystemVersion" : "MinimumOSVersion"] as? String
|
||||
extensionType = (plist["NSExtension"] as? PlistDict)?["NSExtensionPointIdentifier"] as? String
|
||||
icons = Plist_Icon(plist).filenames
|
||||
deviceFamily = parseDeviceFamily(plist, isOSX: isOSX)
|
||||
transportSecurity = plist["NSAppTransportSecurity"] as? PlistDict
|
||||
}
|
||||
}
|
||||
|
||||
private func parseDeviceFamily(_ plist: PlistDict, isOSX: Bool) -> [String] {
|
||||
if isOSX {
|
||||
return plist["CFBundleSupportedPlatforms"] as? [String] ?? ["macOS"]
|
||||
}
|
||||
|
||||
if let platforms = (plist["UIDeviceFamily"] as? [Int])?.compactMap({
|
||||
switch $0 {
|
||||
case 1: "iPhone"
|
||||
case 2: "iPad"
|
||||
case 3: "TV"
|
||||
case 4: "Watch"
|
||||
default: nil
|
||||
}
|
||||
}), platforms.count > 0 {
|
||||
return platforms
|
||||
}
|
||||
|
||||
if let minVersion = plist["MinimumOSVersion"] as? String {
|
||||
if minVersion.hasPrefix("1.") || minVersion.hasPrefix("2.") || minVersion.hasPrefix("3.") {
|
||||
return ["iPhone"]
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
61
src/Data - Apple/Plist+MobileProvision.swift
Normal file
61
src/Data - Apple/Plist+MobileProvision.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `embedded.mobileprovision` (if available) and decode with CMS decoder.
|
||||
func readPlist_MobileProvision() -> Plist_MobileProvision? {
|
||||
guard let provisionData = self.readPayloadFile("embedded.mobileprovision", osxSubdir: nil),
|
||||
let plist = provisionData.decodeCMS().asPlistOrNil() else {
|
||||
return nil
|
||||
}
|
||||
return Plist_MobileProvision(plist, isOSX: self.isOSX)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Plist_MobileProvision
|
||||
|
||||
/// Representation of `embedded.mobileprovision`
|
||||
struct Plist_MobileProvision {
|
||||
let creationDate: Date?
|
||||
let expireDate: Date?
|
||||
let profileId: String?
|
||||
let profileName: String?
|
||||
/// Something like "Development" or "Distribution (App Store)".
|
||||
let profileType: String
|
||||
/// Either "Mac" or "iOS"
|
||||
let profilePlatform: String
|
||||
let teamName: String?
|
||||
let teamIds: [String]
|
||||
let devices: [String]
|
||||
let certificates: [ProvisioningCertificate]
|
||||
let entitlements: PlistDict?
|
||||
|
||||
init(_ plist: PlistDict, isOSX: Bool) {
|
||||
creationDate = plist["CreationDate"] as? Date
|
||||
expireDate = plist["ExpirationDate"] as? Date
|
||||
profileId = plist["UUID"] as? String
|
||||
profileName = plist["Name"] as? String
|
||||
profileType = parseProfileType(plist, isOSX: isOSX)
|
||||
profilePlatform = isOSX ? "Mac" : "iOS"
|
||||
teamName = plist["TeamName"] as? String
|
||||
teamIds = plist["TeamIdentifier"] as? [String] ?? []
|
||||
devices = plist["ProvisionedDevices"] as? [String] ?? []
|
||||
certificates = (plist["DeveloperCertificates"] as? [Data] ?? []).compactMap {
|
||||
ProvisioningCertificate($0)
|
||||
}
|
||||
entitlements = plist["Entitlements"] as? PlistDict
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns provision type string like "Development" or "Distribution (App Store)".
|
||||
private func parseProfileType(_ plist: PlistDict, isOSX: Bool) -> String {
|
||||
let hasDevices = plist["ProvisionedDevices"] is [Any]
|
||||
if isOSX {
|
||||
return hasDevices ? "Development" : "Distribution (App Store)"
|
||||
}
|
||||
if hasDevices {
|
||||
let getTaskAllow = (plist["Entitlements"] as? PlistDict)?["get-task-allow"] as? Bool ?? false
|
||||
return getTaskAllow ? "Development" : "Distribution (Ad Hoc)"
|
||||
}
|
||||
let isEnterprise = plist["ProvisionsAllDevices"] as? Bool ?? false
|
||||
return isEnterprise ? "Enterprise" : "Distribution (App Store)"
|
||||
}
|
||||
77
src/Data - Apple/Plist+iTunesMetadata.swift
Normal file
77
src/Data - Apple/Plist+iTunesMetadata.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `iTunesMetadata.plist` (if available)
|
||||
func readPlist_iTunesMetadata() -> Plist_iTunesMetadata? {
|
||||
assert(type == .IPA)
|
||||
// not `readPayloadFile` because plist is in root dir
|
||||
guard let plist = self.zipFile!.unzipFile("iTunesMetadata.plist")?.asPlistOrNil() else {
|
||||
return nil
|
||||
}
|
||||
return Plist_iTunesMetadata(plist)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Plist_iTunesMetadata
|
||||
|
||||
/// Representation of `iTunesMetadata.plist`
|
||||
struct Plist_iTunesMetadata {
|
||||
let appId: Int?
|
||||
let appName: String?
|
||||
let price: String?
|
||||
let genres: [String]
|
||||
// purchase info
|
||||
let releaseDate: Date?
|
||||
let purchaseDate: Date?
|
||||
// account info
|
||||
let appleId: String?
|
||||
let firstName: String?
|
||||
let lastName: String?
|
||||
|
||||
init(_ plist: PlistDict) {
|
||||
appId = plist["itemId"] as? Int
|
||||
appName = plist["itemName"] as? String
|
||||
price = plist["priceDisplay"] as? String
|
||||
genres = formattedGenres(plist)
|
||||
// download info
|
||||
let downloadInfo = plist["com.apple.iTunesStore.downloadInfo"] as? PlistDict
|
||||
purchaseDate = Date.parseAny(downloadInfo?["purchaseDate"] ?? plist["purchaseDate"])
|
||||
releaseDate = Date.parseAny(downloadInfo?["releaseDate"] ?? plist["releaseDate"])
|
||||
// AppleId & purchaser name
|
||||
let accountInfo = downloadInfo?["accountInfo"] as? PlistDict ?? [:]
|
||||
appleId = accountInfo["AppleID"] as? String ?? plist["appleId"] as? String
|
||||
firstName = accountInfo["FirstName"] as? String
|
||||
lastName = accountInfo["LastName"] as? String
|
||||
}
|
||||
|
||||
/// Returns `"<firstName> <lastName> (<appleId>)"` (with empty values omitted)
|
||||
var purchaserName: String? {
|
||||
let fn = firstName ?? ""
|
||||
let ln = lastName ?? ""
|
||||
let aid = appleId ?? ""
|
||||
switch (fn.isEmpty, ln.isEmpty, aid.isEmpty) {
|
||||
case (true, true, true): return nil
|
||||
case (true, true, false): return "\(aid)"
|
||||
case (_, _, false): return "\(fn) \(ln) (\(aid))"
|
||||
case (_, _, true): return "\(fn) \(ln)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Concatenate all (sub)genres into flat list.
|
||||
private func formattedGenres(_ plist: PlistDict) -> [String] {
|
||||
var genres: [String] = []
|
||||
let genreId = plist["genreId"] as? Int ?? 0
|
||||
if let mainGenre = AppCategories[genreId] ?? plist["genre"] as? String {
|
||||
genres.append(mainGenre)
|
||||
}
|
||||
|
||||
for subgenre in plist["subgenres"] as? [PlistDict] ?? [] {
|
||||
let subgenreId = subgenre["genreId"] as? Int ?? 0
|
||||
if let subgenreStr = AppCategories[subgenreId] ?? subgenre["genre"] as? String {
|
||||
genres.append(subgenreStr)
|
||||
}
|
||||
}
|
||||
return genres
|
||||
}
|
||||
58
src/Data - Apple/Provisioning.swift
Normal file
58
src/Data - Apple/Provisioning.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
import Foundation
|
||||
import os // OSLog
|
||||
|
||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Provisioning")
|
||||
|
||||
extension Data {
|
||||
/// In-memory decode of `embedded.mobileprovision`
|
||||
func decodeCMS() -> Data {
|
||||
var decoder: CMSDecoder? = nil
|
||||
CMSDecoderCreate(&decoder)
|
||||
return self.withUnsafeBytes { ptr in
|
||||
CMSDecoderUpdateMessage(decoder!, ptr.baseAddress!, self.count)
|
||||
CMSDecoderFinalizeMessage(decoder!)
|
||||
var dataRef: CFData?
|
||||
CMSDecoderCopyContent(decoder!, &dataRef)
|
||||
return Data(referencing: dataRef!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProvisioningCertificate {
|
||||
let subject: String
|
||||
let expiration: Date?
|
||||
|
||||
/// Parse subject and expiration date from certificate.
|
||||
init?(_ data: Data) {
|
||||
guard let cert = SecCertificateCreateWithData(nil, data as CFData) else {
|
||||
return nil
|
||||
}
|
||||
guard let subj = SecCertificateCopySubjectSummary(cert) as? String else {
|
||||
os_log(.error, log: log, "Could not get subject from certificate")
|
||||
return nil
|
||||
}
|
||||
subject = subj
|
||||
expiration = parseInvalidityDate(cert, subject: subj)
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a single certificate. Extract invalidity / expiration date.
|
||||
/// @param subject just used for printing error logs.
|
||||
private func parseInvalidityDate(_ certificate: SecCertificate, subject: String) -> Date? {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let outerDict = SecCertificateCopyValues(certificate, [kSecOIDInvalidityDate] as CFArray, &error) as? PlistDict else {
|
||||
os_log(.error, log: log, "Could not get values in '%{public}@' certificate, error = %{public}@", subject, error?.takeUnretainedValue().localizedDescription ?? "unknown error")
|
||||
return nil
|
||||
}
|
||||
guard let innerDict = outerDict[kSecOIDInvalidityDate as String] as? PlistDict else {
|
||||
os_log(.error, log: log, "No invalidity values in '%{public}@' certificate, dictionary = %{public}@", subject, outerDict)
|
||||
return nil
|
||||
}
|
||||
// NOTE: the invalidity date type of kSecPropertyTypeDate is documented as a CFStringRef in the "Certificate, Key, and Trust Services Reference".
|
||||
// In reality, it's a __NSTaggedDate (presumably a tagged pointer representing an NSDate.) But to be sure, we'll check:
|
||||
guard let dateString = innerDict[kSecPropertyKeyValue as String] else {
|
||||
os_log(.error, log: log, "No invalidity date in '%{public}@' certificate, dictionary = %{public}@", subject, innerDict)
|
||||
return nil
|
||||
}
|
||||
return Date.parseAny(dateString)
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import Foundation
|
||||
import os // OSLog
|
||||
|
||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "MetaInfo")
|
||||
|
||||
typealias PlistDict = [String: Any] // basically an untyped Dict
|
||||
|
||||
|
||||
// Init QuickLook Type
|
||||
enum FileType {
|
||||
case IPA
|
||||
case Archive
|
||||
case Extension
|
||||
}
|
||||
|
||||
struct MetaInfo {
|
||||
let UTI: String
|
||||
let url: URL
|
||||
let effectiveUrl: URL? // if set, will point to the app inside of an archive
|
||||
|
||||
let type: FileType
|
||||
let zipFile: ZipFile? // only set for zipped file types
|
||||
let isOSX = false // relict of the past when ProvisionQL also processed provision profiles
|
||||
|
||||
/// 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"
|
||||
|
||||
var effective: URL? = nil
|
||||
var zipFile: ZipFile? = nil
|
||||
|
||||
switch self.UTI {
|
||||
case "com.apple.itunes.ipa", "com.opa334.trollstore.tipa", "dyn.ah62d4rv4ge81k4puqe":
|
||||
self.type = FileType.IPA;
|
||||
zipFile = ZipFile(self.url.path);
|
||||
case "com.apple.xcode.archive":
|
||||
self.type = FileType.Archive;
|
||||
effective = appPathForArchive(self.url);
|
||||
case "com.apple.application-and-system-extension":
|
||||
self.type = FileType.Extension;
|
||||
default:
|
||||
os_log(.error, log: log, "Unsupported file type: %{public}@", self.UTI)
|
||||
fatalError()
|
||||
}
|
||||
self.zipFile = zipFile
|
||||
self.effectiveUrl = effective
|
||||
}
|
||||
|
||||
/// Load a file from bundle into memory. Either by file path or via unzip.
|
||||
func readPayloadFile(_ filename: String) -> Data? {
|
||||
switch (self.type) {
|
||||
case .IPA:
|
||||
return zipFile!.unzipFile("Payload/*.app/".appending(filename))
|
||||
case .Archive:
|
||||
return try? Data(contentsOf: effectiveUrl!.appendingPathComponent(filename))
|
||||
case .Extension:
|
||||
return try? Data(contentsOf: url.appendingPathComponent(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")?.asPlistOrNil()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Plist
|
||||
|
||||
extension Data {
|
||||
/// Helper for optional chaining.
|
||||
func asPlistOrNil() -> PlistDict? {
|
||||
if self.isEmpty {
|
||||
return nil
|
||||
}
|
||||
// var format: PropertyListSerialization.PropertyListFormat = .xml
|
||||
do {
|
||||
return try PropertyListSerialization.propertyList(from: self, format: nil) as? PlistDict
|
||||
} catch {
|
||||
os_log(.error, log: log, "ERROR reading plist %{public}@", error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Meta data for QuickLook
|
||||
|
||||
/// Search an archive for the .app or .ipa bundle.
|
||||
private func appPathForArchive(_ url: URL) -> URL? {
|
||||
let appsDir = url.appendingPathComponent("Products/Applications/")
|
||||
if FileManager.default.fileExists(atPath: appsDir.path) {
|
||||
if let x = try? FileManager.default.contentsOfDirectory(at: appsDir, includingPropertiesForKeys: nil), !x.isEmpty {
|
||||
return x.first
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// Print recursive tree of key-value mappings.
|
||||
private func recursiveDict(_ dictionary: [String: Any], withReplacements replacements: [String: String] = [:], _ level: Int = 0) -> String {
|
||||
var output = ""
|
||||
for (key, value) in dictionary {
|
||||
let localizedKey = replacements[key] ?? key
|
||||
for _ in 0..<level {
|
||||
output += (level == 1) ? "- " : " "
|
||||
}
|
||||
|
||||
if let subDict = value as? [String: Any] {
|
||||
output += "\(localizedKey):<div class=\"list\">\n"
|
||||
output += recursiveDict(subDict, withReplacements: replacements, level + 1)
|
||||
output += "</div>\n"
|
||||
} else if let number = value as? NSNumber {
|
||||
output += "\(localizedKey): \(number.boolValue ? "YES" : "NO")<br />"
|
||||
} else {
|
||||
output += "\(localizedKey): \(value)<br />"
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
extension PreviewGenerator {
|
||||
/// @return List of ATS flags.
|
||||
private func formattedAppTransportSecurity(_ appPlist: PlistDict) -> String {
|
||||
if let value = appPlist["NSAppTransportSecurity"] as? PlistDict {
|
||||
let localizedKeys = [
|
||||
"NSAllowsArbitraryLoads": "Allows Arbitrary Loads",
|
||||
"NSAllowsArbitraryLoadsForMedia": "Allows Arbitrary Loads for Media",
|
||||
"NSAllowsArbitraryLoadsInWebContent": "Allows Arbitrary Loads in Web Content",
|
||||
"NSAllowsLocalNetworking": "Allows Local Networking",
|
||||
"NSExceptionDomains": "Exception Domains",
|
||||
|
||||
"NSIncludesSubdomains": "Includes Subdomains",
|
||||
"NSRequiresCertificateTransparency": "Requires Certificate Transparency",
|
||||
|
||||
"NSExceptionAllowsInsecureHTTPLoads": "Allows Insecure HTTP Loads",
|
||||
"NSExceptionMinimumTLSVersion": "Minimum TLS Version",
|
||||
"NSExceptionRequiresForwardSecrecy": "Requires Forward Secrecy",
|
||||
|
||||
"NSThirdPartyExceptionAllowsInsecureHTTPLoads": "Allows Insecure HTTP Loads",
|
||||
"NSThirdPartyExceptionMinimumTLSVersion": "Minimum TLS Version",
|
||||
"NSThirdPartyExceptionRequiresForwardSecrecy": "Requires Forward Secrecy",
|
||||
]
|
||||
|
||||
return "<div class=\"list\">\(recursiveDict(value, withReplacements: localizedKeys))</div>"
|
||||
}
|
||||
|
||||
let sdkName = appPlist["DTSDKName"] as? String ?? "0"
|
||||
let sdkNumber = Double(sdkName.trimmingCharacters(in: .letters)) ?? 0
|
||||
if sdkNumber < 9.0 {
|
||||
return "Not applicable before iOS 9.0"
|
||||
}
|
||||
return "No exceptions"
|
||||
}
|
||||
|
||||
/// Process info stored in `Info.plist`
|
||||
mutating func procAppInfo(_ appPlist: PlistDict) {
|
||||
var 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.") {
|
||||
platforms = "iPhone"
|
||||
}
|
||||
|
||||
let extensionType = (appPlist["NSExtension"] as? PlistDict)?["NSExtensionPointIdentifier"] as? String
|
||||
self.apply([
|
||||
"AppName": appPlist["CFBundleDisplayName"] as? String ?? appPlist["CFBundleName"] as? String ?? "",
|
||||
"AppVersion": appPlist["CFBundleShortVersionString"] as? String ?? "",
|
||||
"AppBuildVer": appPlist["CFBundleVersion"] as? String ?? "",
|
||||
"AppId": appPlist["CFBundleIdentifier"] as? String ?? "",
|
||||
|
||||
"AppExtensionTypeHidden": extensionType != nil ? "" : CLASS_HIDDEN,
|
||||
"AppExtensionType": extensionType ?? "",
|
||||
|
||||
"AppDeviceFamily": platforms ?? "",
|
||||
"AppSDK": appPlist["DTSDKName"] as? String ?? "",
|
||||
"AppMinOS": minVersion,
|
||||
"AppTransportSecurity": formattedAppTransportSecurity(appPlist),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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)
|
||||
}
|
||||
try! meta.zipFile!.unzipFile("Payload/*.app/\(bundleExecutable)", toDir: tmpPath)
|
||||
return Entitlements(forBinary: tmpPath + "/" + bundleExecutable)
|
||||
case .Archive:
|
||||
return Entitlements(forBinary: meta.effectiveUrl!.path + "/" + bundleExecutable)
|
||||
case .Extension:
|
||||
return Entitlements(forBinary: meta.url.path + "/" + bundleExecutable)
|
||||
}
|
||||
}
|
||||
|
||||
/// Process compiled binary and provision plist to extract `Entitlements`
|
||||
mutating func procEntitlements(_ meta: MetaInfo, _ appPlist: PlistDict, _ provisionPlist: PlistDict?) {
|
||||
var entitlements = readEntitlements(meta, appPlist["CFBundleExecutable"] as? String)
|
||||
entitlements.applyFallbackIfNeeded(provisionPlist?["Entitlements"] as? PlistDict)
|
||||
|
||||
self.apply([
|
||||
"EntitlementsWarningHidden": entitlements.hasError ? "" : CLASS_HIDDEN,
|
||||
"EntitlementsDict": entitlements.html ?? "No Entitlements",
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
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") else {
|
||||
os_log(.info, log: log, "No embedded.mobileprovision file for %{public}@", self.url.path)
|
||||
return nil
|
||||
}
|
||||
|
||||
var decoder: CMSDecoder? = nil
|
||||
CMSDecoderCreate(&decoder)
|
||||
let data = provisionData.withUnsafeBytes { ptr in
|
||||
CMSDecoderUpdateMessage(decoder!, ptr.baseAddress!, provisionData.count)
|
||||
CMSDecoderFinalizeMessage(decoder!)
|
||||
var dataRef: CFData?
|
||||
CMSDecoderCopyContent(decoder!, &dataRef)
|
||||
return Data(referencing: dataRef!)
|
||||
}
|
||||
return data.asPlistOrNil()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension PreviewGenerator {
|
||||
|
||||
// MARK: - Certificates
|
||||
|
||||
/// Process a single certificate. Extract invalidity / expiration date.
|
||||
/// @param subject just used for printing error logs.
|
||||
private func getCertificateInvalidityDate(_ certificate: SecCertificate, subject: String) -> Date? {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let outerDict = SecCertificateCopyValues(certificate, [kSecOIDInvalidityDate] as CFArray, &error) as? PlistDict else {
|
||||
os_log(.error, log: log, "Could not get values in '%{public}@' certificate, error = %{public}@", subject, error?.takeUnretainedValue().localizedDescription ?? "unknown error")
|
||||
return nil
|
||||
}
|
||||
guard let innerDict = outerDict[kSecOIDInvalidityDate as String] as? PlistDict else {
|
||||
os_log(.error, log: log, "No invalidity values in '%{public}@' certificate, dictionary = %{public}@", subject, outerDict)
|
||||
return nil
|
||||
}
|
||||
// NOTE: the invalidity date type of kSecPropertyTypeDate is documented as a CFStringRef in the "Certificate, Key, and Trust Services Reference".
|
||||
// In reality, it's a __NSTaggedDate (presumably a tagged pointer representing an NSDate.) But to be sure, we'll check:
|
||||
guard let dateString = innerDict[kSecPropertyKeyValue as String] else {
|
||||
os_log(.error, log: log, "No invalidity date in '%{public}@' certificate, dictionary = %{public}@", subject, innerDict)
|
||||
return nil
|
||||
}
|
||||
return Date.parseAny(dateString);
|
||||
}
|
||||
|
||||
/// Process list of all certificates. Return a two column table with subject and expiration date.
|
||||
private func getCertificateList(_ provisionPlist: PlistDict) -> [TableRow] {
|
||||
guard let certs = provisionPlist["DeveloperCertificates"] as? [Data] else {
|
||||
return []
|
||||
}
|
||||
return certs.compactMap {
|
||||
guard let cert = SecCertificateCreateWithData(nil, $0 as CFData) else {
|
||||
return nil
|
||||
}
|
||||
guard let subject = SecCertificateCopySubjectSummary(cert) as? String else {
|
||||
os_log(.error, log: log, "Could not get subject from certificate")
|
||||
return nil
|
||||
}
|
||||
let expiration: String
|
||||
if let invalidityDate = getCertificateInvalidityDate(cert, subject: subject) {
|
||||
expiration = invalidityDate.relativeExpirationDateString()
|
||||
} else {
|
||||
expiration = "<span class='warning'>No invalidity date in certificate</span>"
|
||||
}
|
||||
return TableRow([subject, expiration])
|
||||
}.sorted { $0[0] < $1[0] }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Provisioning
|
||||
|
||||
/// Returns provision type string like "Development" or "Distribution (App Store)".
|
||||
private func stringForProfileType(_ provisionPlist: PlistDict, isOSX: Bool) -> String {
|
||||
let hasDevices = provisionPlist["ProvisionedDevices"] is [Any]
|
||||
if isOSX {
|
||||
return hasDevices ? "Development" : "Distribution (App Store)"
|
||||
}
|
||||
if hasDevices {
|
||||
let getTaskAllow = (provisionPlist["Entitlements"] as? PlistDict)?["get-task-allow"] as? Bool ?? false
|
||||
return getTaskAllow ? "Development" : "Distribution (Ad Hoc)"
|
||||
}
|
||||
let isEnterprise = provisionPlist["ProvisionsAllDevices"] as? Bool ?? false
|
||||
return isEnterprise ? "Enterprise" : "Distribution (App Store)"
|
||||
}
|
||||
|
||||
/// Enumerate all entries from provison plist with key `ProvisionedDevices`
|
||||
private func getDeviceList(_ provisionPlist: PlistDict) -> [TableRow] {
|
||||
guard let devArr = provisionPlist["ProvisionedDevices"] as? [String] else {
|
||||
return []
|
||||
}
|
||||
var currentPrefix: String? = nil
|
||||
return devArr.sorted().map { device in
|
||||
// compute the prefix for the first column of the table
|
||||
let displayPrefix: String
|
||||
let devicePrefix = String(device.prefix(1))
|
||||
if currentPrefix != devicePrefix {
|
||||
currentPrefix = devicePrefix
|
||||
displayPrefix = "\(devicePrefix) ➞ "
|
||||
} else {
|
||||
displayPrefix = ""
|
||||
}
|
||||
return [displayPrefix, device]
|
||||
}
|
||||
}
|
||||
|
||||
/// Process info stored in `embedded.mobileprovision`
|
||||
mutating func procProvision(_ provisionPlist: PlistDict?, isOSX: Bool) {
|
||||
guard let provisionPlist else {
|
||||
self.apply(["ProvisionHidden": CLASS_HIDDEN])
|
||||
return
|
||||
}
|
||||
|
||||
let creationDate = provisionPlist["CreationDate"] as? Date
|
||||
let expireDate = provisionPlist["ExpirationDate"] as? Date
|
||||
let devices = getDeviceList(provisionPlist)
|
||||
let certs = getCertificateList(provisionPlist)
|
||||
|
||||
self.apply([
|
||||
"ProvisionHidden": "",
|
||||
"ProvisionProfileName": provisionPlist["Name"] as? String ?? "",
|
||||
"ProvisionProfileId": provisionPlist["UUID"] as? String ?? "",
|
||||
"ProvisionTeamName": provisionPlist["TeamName"] as? String ?? "<em>Team name not available</em>",
|
||||
"ProvisionTeamIds": (provisionPlist["TeamIdentifier"] as? [String])?.joined(separator: ", ") ?? "<em>Team ID not available</em>",
|
||||
"ProvisionCreateDate": creationDate?.formattedCreationDate() ?? "",
|
||||
"ProvisionExpireDate": expireDate?.formattedExpirationDate() ?? "",
|
||||
"ProvisionExpireStatus": ExpirationStatus(expireDate).cssClass(),
|
||||
|
||||
"ProvisionProfilePlatform": isOSX ? "Mac" : "iOS",
|
||||
"ProvisionProfileType": stringForProfileType(provisionPlist, isOSX: isOSX),
|
||||
|
||||
"ProvisionDeviceCount": devices.isEmpty ? "No Devices" : "\(devices.count) Device\(devices.count == 1 ? "" : "s")",
|
||||
"ProvisionDeviceIds": devices.isEmpty ? "Distribution Profile" : formatAsTable(devices, header: ["", "UDID"]),
|
||||
|
||||
"ProvisionDevelopCertificates": certs.isEmpty ? "No Developer Certificates" : formatAsTable(certs),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private typealias TableRow = [String]
|
||||
|
||||
/// Print html table with arbitrary number of columns
|
||||
/// @param header If set, start the table with a `tr` column row.
|
||||
private func formatAsTable(_ data: [[String]], header: TableRow? = nil) -> String {
|
||||
var table = "<table>\n"
|
||||
if let header = header {
|
||||
table += "<tr><th>\(header.joined(separator: "</th><th>"))</th></tr>\n"
|
||||
}
|
||||
for row in data {
|
||||
table += "<tr><td>\(row.joined(separator: "</td><td>"))</td></tr>\n"
|
||||
}
|
||||
return table + "</table>\n"
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension PreviewGenerator {
|
||||
/// Concatenate all (sub)genres into a comma separated list.
|
||||
private func formattedGenres(_ itunesPlist: PlistDict) -> String {
|
||||
var genres: [String] = []
|
||||
let genreId = itunesPlist["genreId"] as? Int ?? 0
|
||||
if let mainGenre = AppCategories[genreId] ?? itunesPlist["genre"] as? String {
|
||||
genres.append(mainGenre)
|
||||
}
|
||||
|
||||
for subgenre in itunesPlist["subgenres"] as? [PlistDict] ?? [] {
|
||||
let subgenreId = subgenre["genreId"] as? Int ?? 0
|
||||
if let subgenreStr = AppCategories[subgenreId] ?? subgenre["genre"] as? String {
|
||||
genres.append(subgenreStr)
|
||||
}
|
||||
}
|
||||
return genres.joined(separator: ", ")
|
||||
}
|
||||
|
||||
/// Process info stored in `iTunesMetadata.plist`
|
||||
mutating func procItunesMeta(_ itunesPlist: PlistDict?) {
|
||||
guard let itunesPlist else {
|
||||
self.apply(["iTunesHidden": CLASS_HIDDEN])
|
||||
return
|
||||
}
|
||||
|
||||
let downloadInfo = itunesPlist["com.apple.iTunesStore.downloadInfo"] as? PlistDict
|
||||
let accountInfo = downloadInfo?["accountInfo"] as? PlistDict ?? [:]
|
||||
|
||||
let purchaseDate = Date.parseAny(downloadInfo?["purchaseDate"] ?? itunesPlist["purchaseDate"])
|
||||
let releaseDate = Date.parseAny(downloadInfo?["releaseDate"] ?? itunesPlist["releaseDate"])
|
||||
// AppleId & purchaser name
|
||||
let appleId = accountInfo["AppleID"] as? String ?? itunesPlist["appleId"] as? String ?? ""
|
||||
let firstName = accountInfo["FirstName"] as? String ?? ""
|
||||
let lastName = accountInfo["LastName"] as? String ?? ""
|
||||
|
||||
let name: String
|
||||
if !firstName.isEmpty || !lastName.isEmpty {
|
||||
name = "\(firstName) \(lastName) (\(appleId))"
|
||||
} else {
|
||||
name = appleId
|
||||
}
|
||||
self.apply([
|
||||
"iTunesHidden": "",
|
||||
"iTunesId": (itunesPlist["itemId"] as? Int)?.description ?? "",
|
||||
"iTunesName": itunesPlist["itemName"] as? String ?? "",
|
||||
"iTunesGenres": formattedGenres(itunesPlist),
|
||||
"iTunesReleaseDate": releaseDate?.mediumFormat() ?? "",
|
||||
|
||||
"iTunesAppleId": name,
|
||||
"iTunesPurchaseDate": purchaseDate?.mediumFormat() ?? "",
|
||||
"iTunesPrice": itunesPlist["priceDisplay"] as? String ?? "",
|
||||
])
|
||||
}
|
||||
}
|
||||
55
src/Preview/Preview+AppInfo.swift
Normal file
55
src/Preview/Preview+AppInfo.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
|
||||
extension PreviewGenerator {
|
||||
/// Process info stored in `Info.plist`
|
||||
mutating func procAppInfoApple(_ appPlist: Plist_Info) {
|
||||
self.apply([
|
||||
"AppInfoHidden": CLASS_VISIBLE,
|
||||
"AppName": appPlist.name ?? "",
|
||||
"AppVersion": appPlist.version ?? "",
|
||||
"AppBuildVer": appPlist.buildVersion ?? "",
|
||||
"AppId": appPlist.bundleId ?? "",
|
||||
|
||||
"AppExtensionTypeHidden": appPlist.extensionType != nil ? CLASS_VISIBLE : CLASS_HIDDEN,
|
||||
"AppExtensionType": appPlist.extensionType ?? "",
|
||||
|
||||
"AppDeviceFamily": appPlist.deviceFamily.joined(separator: ", "),
|
||||
"AppSDK": appPlist.sdkVersion ?? "",
|
||||
"AppMinOS": appPlist.minOS ?? "",
|
||||
])
|
||||
}
|
||||
|
||||
/// Process info stored in `AndroidManifest.xml`
|
||||
mutating func procAppInfoAndroid(_ manifest: Apk_Manifest) {
|
||||
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: Int?) -> String {
|
||||
sdk == nil ? "" : "\(sdk!) (Android \(AndroidSdkMap[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),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
28
src/Preview/Preview+ArchiveInfo.swift
Normal file
28
src/Preview/Preview+ArchiveInfo.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `Info.plist` if type `.Archive`
|
||||
func readPlistXCArchive() -> PlistDict? {
|
||||
switch self.type {
|
||||
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, .APK:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
self.apply([
|
||||
"ArchiveHidden": CLASS_VISIBLE,
|
||||
"ArchiveComment": comment,
|
||||
])
|
||||
}
|
||||
}
|
||||
39
src/Preview/Preview+Entitlements.swift
Normal file
39
src/Preview/Preview+Entitlements.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
import Foundation
|
||||
|
||||
extension PreviewGenerator {
|
||||
/// Search for app binary and run `codesign` on it.
|
||||
private func readEntitlements(_ meta: MetaInfo, _ bundleExecutable: String?) -> Entitlements {
|
||||
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
|
||||
}
|
||||
}
|
||||
return Entitlements.withoutBinary()
|
||||
}
|
||||
|
||||
/// Process compiled binary and provision plist to extract `Entitlements`
|
||||
mutating func procEntitlements(_ meta: MetaInfo, _ appPlist: Plist_Info?, _ provisionPlist: Plist_MobileProvision?) {
|
||||
var entitlements = readEntitlements(meta, appPlist?.exePath)
|
||||
entitlements.applyFallbackIfNeeded(provisionPlist?.entitlements)
|
||||
|
||||
if entitlements.html == nil && !entitlements.hasError {
|
||||
return
|
||||
}
|
||||
|
||||
self.apply([
|
||||
"EntitlementsHidden" : CLASS_VISIBLE,
|
||||
"EntitlementsWarningHidden": entitlements.hasError ? CLASS_VISIBLE : CLASS_HIDDEN,
|
||||
"EntitlementsDict": entitlements.html ?? "No Entitlements",
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
extension PreviewGenerator {
|
||||
/// Calculate file / folder size.
|
||||
private func getFileSize(_ path: String) -> Int64 {
|
||||
var isDir: ObjCBool = false
|
||||
FileManager.default.fileExists(atPath: path, isDirectory: &isDir)
|
||||
if !isDir.boolValue {
|
||||
return try! FileManager.default.attributesOfItem(atPath: path)[.size] as! Int64
|
||||
}
|
||||
var fileSize: Int64 = 0
|
||||
for child in try! FileManager.default.subpathsOfDirectory(atPath: path) {
|
||||
fileSize += try! FileManager.default.attributesOfItem(atPath: path + "/" + child)[.size] as! Int64
|
||||
}
|
||||
return fileSize
|
||||
}
|
||||
|
||||
/// Process meta information about the file itself. Like file size and last modification.
|
||||
mutating func procFileInfo(_ url: URL) {
|
||||
let attrs = try? FileManager.default.attributesOfItem(atPath: url.path)
|
||||
self.apply([
|
||||
"FileName": escapeXML(url.lastPathComponent),
|
||||
"FileSize": ByteCountFormatter.string(fromByteCount: getFileSize(url.path), countStyle: .file),
|
||||
"FileModified": (attrs?[.modificationDate] as? Date)?.mediumFormat() ?? "",
|
||||
"FileSize": url.fileSizeHuman(),
|
||||
"FileModified": url.modificationDate()?.mediumFormat() ?? "",
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -36,3 +21,32 @@ private func escapeXML(_ stringToEscape: String) -> String {
|
||||
.replacingOccurrences(of: "<", with: "<")
|
||||
.replacingOccurrences(of: ">", with: ">")
|
||||
}
|
||||
|
||||
|
||||
extension URL {
|
||||
/// Last modification date of file (or folder)
|
||||
@inlinable func modificationDate() -> Date? {
|
||||
(try? FileManager.default.attributesOfItem(atPath: self.path))?[.modificationDate] as? Date
|
||||
}
|
||||
|
||||
/// Calls `fileSize()`. Will convert `Int` to human readable `String`.
|
||||
func fileSizeHuman() -> String {
|
||||
ByteCountFormatter.string(fromByteCount: self.fileSize(), countStyle: .file)
|
||||
}
|
||||
|
||||
// MARK: - private methods
|
||||
|
||||
/// Calculate file or folder size.
|
||||
private func fileSize() -> Int64 {
|
||||
var isDir: ObjCBool = false
|
||||
FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir)
|
||||
if !isDir.boolValue {
|
||||
return try! FileManager.default.attributesOfItem(atPath: self.path)[.size] as! Int64
|
||||
}
|
||||
var fileSize: Int64 = 0
|
||||
for child in try! FileManager.default.subpathsOfDirectory(atPath: self.path) {
|
||||
fileSize += try! FileManager.default.attributesOfItem(atPath: self.path + "/" + child)[.size] as! Int64
|
||||
}
|
||||
return fileSize
|
||||
}
|
||||
}
|
||||
68
src/Preview/Preview+Provisioning.swift
Normal file
68
src/Preview/Preview+Provisioning.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
import Foundation
|
||||
|
||||
extension PreviewGenerator {
|
||||
/// Process info stored in `embedded.mobileprovision`
|
||||
mutating func procProvision(_ provisionPlist: Plist_MobileProvision?) {
|
||||
guard let provisionPlist else {
|
||||
return
|
||||
}
|
||||
|
||||
let deviceCount = provisionPlist.devices.count
|
||||
self.apply([
|
||||
"ProvisionHidden": CLASS_VISIBLE,
|
||||
"ProvisionProfileName": provisionPlist.profileName ?? "",
|
||||
"ProvisionProfileId": provisionPlist.profileId ?? "",
|
||||
"ProvisionTeamName": provisionPlist.teamName ?? "<em>Team name not available</em>",
|
||||
"ProvisionTeamIds": provisionPlist.teamIds.isEmpty ? "<em>Team ID not available</em>" : provisionPlist.teamIds.joined(separator: ", "),
|
||||
"ProvisionCreateDate": provisionPlist.creationDate?.formattedCreationDate() ?? "",
|
||||
"ProvisionExpireDate": provisionPlist.expireDate?.formattedExpirationDate() ?? "",
|
||||
"ProvisionExpireStatus": ExpirationStatus(provisionPlist.expireDate).cssClass(),
|
||||
|
||||
"ProvisionProfilePlatform": provisionPlist.profilePlatform,
|
||||
"ProvisionProfileType": provisionPlist.profileType,
|
||||
|
||||
"ProvisionDeviceCount": deviceCount == 0 ? "No Devices" : "\(deviceCount) Device\(deviceCount == 1 ? "" : "s")",
|
||||
"ProvisionDeviceIds": deviceCount == 0 ? "Distribution Profile" : formatAsTable(groupDevices(provisionPlist.devices), header: ["", "UDID"]),
|
||||
|
||||
"ProvisionDevelopCertificates": provisionPlist.certificates.isEmpty ? "No Developer Certificates"
|
||||
: formatAsTable(
|
||||
provisionPlist.certificates
|
||||
.sorted { $0.subject < $1.subject }
|
||||
.map {TableRow([$0.subject, $0.expiration?.relativeExpirationDateString() ?? "<span class='warning'>No invalidity date in certificate</span>"])}
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// Group device ids by first letter (`d -> device02`)
|
||||
private func groupDevices(_ devices: [String]) -> [TableRow] {
|
||||
var currentPrefix: String? = nil
|
||||
return devices.sorted().map { device in
|
||||
// compute the prefix for the first column of the table
|
||||
let displayPrefix: String
|
||||
let devicePrefix = String(device.prefix(1))
|
||||
if currentPrefix != devicePrefix {
|
||||
currentPrefix = devicePrefix
|
||||
displayPrefix = "\(devicePrefix) ➞ "
|
||||
} else {
|
||||
displayPrefix = ""
|
||||
}
|
||||
return [displayPrefix, device]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private typealias TableRow = [String]
|
||||
|
||||
/// Print html table with arbitrary number of columns
|
||||
/// @param header If set, start the table with a `tr` column row.
|
||||
private func formatAsTable(_ data: [[String]], header: TableRow? = nil) -> String {
|
||||
var table = "<table>\n"
|
||||
if let header = header {
|
||||
table += "<tr><th>\(header.joined(separator: "</th><th>"))</th></tr>\n"
|
||||
}
|
||||
for row in data {
|
||||
table += "<tr><td>\(row.joined(separator: "</td><td>"))</td></tr>\n"
|
||||
}
|
||||
return table + "</table>\n"
|
||||
}
|
||||
56
src/Preview/Preview+TransportSecurity.swift
Normal file
56
src/Preview/Preview+TransportSecurity.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
import Foundation
|
||||
|
||||
private let TransportSecurityLocalizedKeys = [
|
||||
"NSAllowsArbitraryLoads": "Allows Arbitrary Loads",
|
||||
"NSAllowsArbitraryLoadsForMedia": "Allows Arbitrary Loads for Media",
|
||||
"NSAllowsArbitraryLoadsInWebContent": "Allows Arbitrary Loads in Web Content",
|
||||
"NSAllowsLocalNetworking": "Allows Local Networking",
|
||||
"NSExceptionDomains": "Exception Domains",
|
||||
|
||||
"NSIncludesSubdomains": "Includes Subdomains",
|
||||
"NSRequiresCertificateTransparency": "Requires Certificate Transparency",
|
||||
|
||||
"NSExceptionAllowsInsecureHTTPLoads": "Allows Insecure HTTP Loads",
|
||||
"NSExceptionMinimumTLSVersion": "Minimum TLS Version",
|
||||
"NSExceptionRequiresForwardSecrecy": "Requires Forward Secrecy",
|
||||
|
||||
"NSThirdPartyExceptionAllowsInsecureHTTPLoads": "Allows Insecure HTTP Loads",
|
||||
"NSThirdPartyExceptionMinimumTLSVersion": "Minimum TLS Version",
|
||||
"NSThirdPartyExceptionRequiresForwardSecrecy": "Requires Forward Secrecy",
|
||||
]
|
||||
|
||||
/// Print recursive tree of key-value mappings.
|
||||
private func recursiveTransportSecurity(_ dictionary: PlistDict, _ level: Int = 0) -> String {
|
||||
var output = ""
|
||||
for (key, value) in dictionary {
|
||||
let localizedKey = TransportSecurityLocalizedKeys[key] ?? key
|
||||
for _ in 0..<level {
|
||||
output += (level == 1) ? "- " : " "
|
||||
}
|
||||
|
||||
if let subDict = value as? [String: Any] {
|
||||
output += "\(localizedKey):<div class=\"list\">\n"
|
||||
output += recursiveTransportSecurity(subDict, level + 1)
|
||||
output += "</div>\n"
|
||||
} else if let number = value as? NSNumber {
|
||||
output += "\(localizedKey): \(number.boolValue ? "YES" : "NO")<br />"
|
||||
} else {
|
||||
output += "\(localizedKey): \(value)<br />"
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
extension PreviewGenerator {
|
||||
/// Process ATS info in `Info.plist`
|
||||
mutating func procTransportSecurity(_ appPlist: Plist_Info?) {
|
||||
guard let value = appPlist?.transportSecurity else {
|
||||
return
|
||||
}
|
||||
|
||||
self.apply([
|
||||
"TransportSecurityHidden": CLASS_VISIBLE,
|
||||
"TransportSecurityDict": "<div class=\"list\">\(recursiveTransportSecurity(value))</div>",
|
||||
])
|
||||
}
|
||||
}
|
||||
21
src/Preview/Preview+iTunesPurchase.swift
Normal file
21
src/Preview/Preview+iTunesPurchase.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
|
||||
extension PreviewGenerator {
|
||||
/// Process info stored in `iTunesMetadata.plist`
|
||||
mutating func procItunesMeta(_ itunesPlist: Plist_iTunesMetadata?) {
|
||||
guard let itunesPlist else {
|
||||
return
|
||||
}
|
||||
self.apply([
|
||||
"iTunesHidden": CLASS_VISIBLE,
|
||||
"iTunesId": itunesPlist.appId?.description ?? "",
|
||||
"iTunesName": itunesPlist.appName ?? "",
|
||||
"iTunesGenres": itunesPlist.genres.joined(separator: ", "),
|
||||
"iTunesReleaseDate": itunesPlist.releaseDate?.mediumFormat() ?? "",
|
||||
|
||||
"iTunesAppleId": itunesPlist.purchaserName ?? "",
|
||||
"iTunesPurchaseDate": itunesPlist.purchaseDate?.mediumFormat() ?? "",
|
||||
"iTunesPrice": itunesPlist.price ?? "",
|
||||
])
|
||||
}
|
||||
}
|
||||
105
src/Preview/PreviewGenerator.swift
Normal file
105
src/Preview/PreviewGenerator.swift
Normal file
@@ -0,0 +1,105 @@
|
||||
import Foundation
|
||||
|
||||
let CLASS_HIDDEN = "hidden"
|
||||
let CLASS_VISIBLE = ""
|
||||
|
||||
struct PreviewGenerator {
|
||||
/// 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
|
||||
|
||||
switch meta.type {
|
||||
case .IPA, .Archive, .Extension:
|
||||
guard let plistApp = meta.readPlist_Info() else {
|
||||
throw RuntimeError("Info.plist not found")
|
||||
}
|
||||
procAppInfoApple(plistApp)
|
||||
if meta.type == .IPA {
|
||||
procItunesMeta(meta.readPlist_iTunesMetadata())
|
||||
} else if meta.type == .Archive {
|
||||
procArchiveInfo(meta.readPlistXCArchive())
|
||||
}
|
||||
procTransportSecurity(plistApp)
|
||||
|
||||
let plistProvision = meta.readPlist_MobileProvision()
|
||||
procEntitlements(meta, plistApp, plistProvision)
|
||||
procProvision(plistProvision)
|
||||
// App Icon (last, because the image uses a lot of memory)
|
||||
data["AppIcon"] = AppIcon(meta).extractImage(from: plistApp.icons).withRoundCorners().asBase64()
|
||||
|
||||
case .APK:
|
||||
guard let manifest = meta.readApk_Manifest() 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.icon).withRoundCorners().asBase64()
|
||||
}
|
||||
|
||||
data["QuickLookTitle"] = stringForFileType(meta)
|
||||
procFileInfo(meta.url)
|
||||
procFooterInfo()
|
||||
}
|
||||
|
||||
mutating func apply(_ values: [String: String]) {
|
||||
data.merge(values) { (_, new) in new }
|
||||
}
|
||||
|
||||
/// Title of the preview window
|
||||
private func stringForFileType(_ meta: MetaInfo) -> String {
|
||||
switch meta.type {
|
||||
case .IPA, .APK: return "App info"
|
||||
case .Archive: return "Archive info"
|
||||
case .Extension: return "App extension info"
|
||||
}
|
||||
}
|
||||
|
||||
/// prepare html, replace values
|
||||
func generate(template html: String, css: String) -> String {
|
||||
let templateValues = data.merging(["CSS": css]) { (_, new) in new }
|
||||
return html.regexReplace("\\{\\{([^ }]{1,40}?)\\}\\}") { templateValues[$0] }
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
/// Replace regex-pattern with custom replacement.
|
||||
/// @param pattern must include a regex group. (e.g. "a(b)c")
|
||||
func regexReplace(_ pattern: String, with fn: (_ match: String) -> String?) -> String {
|
||||
var rv = ""
|
||||
var prevLoc = self.startIndex
|
||||
let regex = try! NSRegularExpression(pattern: pattern)
|
||||
regex.enumerateMatches(in: self, range: NSRange(location: 0, length: self.count), using: { match, flags, stop in
|
||||
let start = self.index(self.startIndex, offsetBy: match!.range.lowerBound)
|
||||
// append unrelated text up to this key
|
||||
rv.append(contentsOf: self[prevLoc ..< start])
|
||||
prevLoc = self.index(start, offsetBy: match!.range.length)
|
||||
// append key if exists (else remove template-key)
|
||||
let key = String(self[Range(match!.range(at: 1), in: self)!])
|
||||
if let value = fn(key) {
|
||||
rv.append(value)
|
||||
} else {
|
||||
// do not append anything -> removes all template keys from template
|
||||
// os_log(.debug, log: log, "unknown template key: %{public}@", key)
|
||||
}
|
||||
})
|
||||
// append remaining text
|
||||
rv.append(contentsOf: self[prevLoc ..< self.endIndex])
|
||||
return rv
|
||||
}
|
||||
}
|
||||
14
src/Preview/RuntimeError.swift
Normal file
14
src/Preview/RuntimeError.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
import Foundation
|
||||
|
||||
// used to quit QuickLook generation without returning a valid preview
|
||||
struct RuntimeError: LocalizedError {
|
||||
let description: String
|
||||
|
||||
init(_ description: String) {
|
||||
self.description = description
|
||||
}
|
||||
|
||||
var errorDescription: String? {
|
||||
description
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
let CLASS_HIDDEN = "hiddenDiv"
|
||||
|
||||
struct PreviewGenerator {
|
||||
var data: [String: String] = [:] // used for TAG replacements
|
||||
let meta: MetaInfo
|
||||
|
||||
init(_ meta: MetaInfo) {
|
||||
self.meta = meta
|
||||
guard let plistApp = meta.readPlistApp() else {
|
||||
return
|
||||
}
|
||||
let plistItunes = meta.readPlistItunes()
|
||||
let plistProvision = meta.readPlistProvision()
|
||||
|
||||
data["QuickLookTitle"] = stringForFileType(meta)
|
||||
|
||||
procAppInfo(plistApp)
|
||||
procItunesMeta(plistItunes)
|
||||
procProvision(plistProvision, isOSX: meta.isOSX)
|
||||
procEntitlements(meta, plistApp, plistProvision)
|
||||
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]) {
|
||||
data.merge(values) { (_, new) in new }
|
||||
}
|
||||
|
||||
/// Title of the preview window
|
||||
private func stringForFileType(_ meta: MetaInfo) -> String {
|
||||
switch meta.type {
|
||||
case .IPA: return "App info"
|
||||
case .Archive: return "Archive info"
|
||||
case .Extension: return "App extension info"
|
||||
}
|
||||
}
|
||||
|
||||
/// prepare html, replace values
|
||||
func generate(template html: String, css: String) -> String {
|
||||
let templateValues = data.merging(["CSS": css]) { (_, new) in new }
|
||||
return html.regexReplace("__([^ _]{1,40}?)__") { templateValues[$0] }
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
/// Replace regex-pattern with custom replacement.
|
||||
/// @param pattern must include a regex group. (e.g. "a(b)c")
|
||||
func regexReplace(_ pattern: String, with fn: (_ match: String) -> String?) -> String {
|
||||
var rv = ""
|
||||
var prevLoc = self.startIndex
|
||||
let regex = try! NSRegularExpression(pattern: pattern)
|
||||
regex.enumerateMatches(in: self, range: NSRange(location: 0, length: self.count), using: { match, flags, stop in
|
||||
let start = self.index(self.startIndex, offsetBy: match!.range.lowerBound)
|
||||
// append unrelated text up to this key
|
||||
rv.append(contentsOf: self[prevLoc ..< start])
|
||||
prevLoc = self.index(start, offsetBy: match!.range.length)
|
||||
// append key if exists (else remove template-key)
|
||||
let key = String(self[Range(match!.range(at: 1), in: self)!])
|
||||
if let value = fn(key) {
|
||||
rv.append(value)
|
||||
} else {
|
||||
// os_log(.debug, log: log, "unknown template key: %{public}@", key)
|
||||
}
|
||||
})
|
||||
// append remaining text
|
||||
rv.append(contentsOf: self[prevLoc ..< self.endIndex])
|
||||
return rv
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user