feat: initial Swift version
This commit is contained in:
1
PrivateFrameworks/CoreUI.framework/Headers
Symbolic link
1
PrivateFrameworks/CoreUI.framework/Headers
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Headers/
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@class NSBundle, NSCache, NSMapTable, NSString;
|
||||||
|
@class CUINamedImage, CUIStructuredThemeStore;
|
||||||
|
|
||||||
|
@interface CUICatalog: NSObject {
|
||||||
|
NSString * _assetStoreName;
|
||||||
|
NSBundle * _bundle;
|
||||||
|
unsigned int _fileHasDisplayGamutInKeySpace;
|
||||||
|
NSCache * _localObjectCache;
|
||||||
|
NSCache * _lookupCache;
|
||||||
|
NSCache * _negativeCache;
|
||||||
|
unsigned short _preferredLocalization;
|
||||||
|
unsigned int _purgeWhenFinished;
|
||||||
|
unsigned int _reserved;
|
||||||
|
NSMapTable * _storageMapTable;
|
||||||
|
unsigned long long _storageRef;
|
||||||
|
NSDictionary * _vibrantColorMatrixTints;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CUIStructuredThemeStore *)_themeStore;
|
||||||
|
|
||||||
|
+ (id)defaultUICatalogForBundle:(id)arg1;
|
||||||
|
|
||||||
|
- (id)initWithBytes:(const void*)arg1 length:(unsigned long long)arg2 error:(NSError **)arg3;
|
||||||
|
- (id)initWithName:(id)arg1 fromBundle:(id)arg2;
|
||||||
|
- (id)initWithName:(id)arg1 fromBundle:(id)arg2 error:(id*)arg3;
|
||||||
|
- (id)initWithURL:(id)arg1 error:(NSError **)arg2;
|
||||||
|
|
||||||
|
- (BOOL)imageExistsWithName:(id)arg1;
|
||||||
|
- (BOOL)imageExistsWithName:(id)arg1 scaleFactor:(double)arg2;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 appearanceName:(id)arg3;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3 appearanceName:(id)arg4;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3 deviceSubtype:(unsigned long long)arg4;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3 deviceSubtype:(unsigned long long)arg4 appearanceName:(id)arg5;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3 deviceSubtype:(unsigned long long)arg4 displayGamut:(long long)arg5 layoutDirection:(long long)arg6 sizeClassHorizontal:(long long)arg7 sizeClassVertical:(long long)arg8;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3 deviceSubtype:(unsigned long long)arg4 displayGamut:(long long)arg5 layoutDirection:(long long)arg6 sizeClassHorizontal:(long long)arg7 sizeClassVertical:(long long)arg8 appearanceName:(id)arg9;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3 deviceSubtype:(unsigned long long)arg4 displayGamut:(long long)arg5 layoutDirection:(long long)arg6 sizeClassHorizontal:(long long)arg7 sizeClassVertical:(long long)arg8 memoryClass:(long long)arg9 graphicsClass:(long long)arg10;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3 deviceSubtype:(unsigned long long)arg4 displayGamut:(long long)arg5 layoutDirection:(long long)arg6 sizeClassHorizontal:(long long)arg7 sizeClassVertical:(long long)arg8 memoryClass:(unsigned long long)arg9 graphicsClass:(unsigned long long)arg10 appearanceIdentifier:(long long)arg11 graphicsFallBackOrder:(id)arg12 deviceSubtypeFallBackOrder:(id)arg13;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3 deviceSubtype:(unsigned long long)arg4 displayGamut:(long long)arg5 layoutDirection:(long long)arg6 sizeClassHorizontal:(long long)arg7 sizeClassVertical:(long long)arg8 memoryClass:(unsigned long long)arg9 graphicsClass:(unsigned long long)arg10 graphicsFallBackOrder:(id)arg11 deviceSubtypeFallBackOrder:(id)arg12;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3 deviceSubtype:(unsigned long long)arg4 sizeClassHorizontal:(long long)arg5 sizeClassVertical:(long long)arg6;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3 deviceSubtype:(unsigned long long)arg4 sizeClassHorizontal:(long long)arg5 sizeClassVertical:(long long)arg6 appearanceName:(id)arg7;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 deviceIdiom:(long long)arg3 layoutDirection:(long long)arg4 adjustRenditionKeyWithBlock:(id)arg5;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 displayGamut:(long long)arg3 layoutDirection:(long long)arg4;
|
||||||
|
- (CUINamedImage *)imageWithName:(id)arg1 scaleFactor:(double)arg2 displayGamut:(long long)arg3 layoutDirection:(long long)arg4 appearanceName:(id)arg5;
|
||||||
|
- (NSArray<CUINamedImage *> *)imagesWithName:(id)arg1;
|
||||||
|
|
||||||
|
- (NSArray<NSString *> *)allImageNames;
|
||||||
|
- (NSArray<NSString *> *)appearanceNames;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <CoreUI/CUINamedLookup.h>
|
||||||
|
#import <CoreGraphics/CoreGraphics.h>
|
||||||
|
|
||||||
|
@interface CUINamedImage: CUINamedLookup {
|
||||||
|
struct _cuiniproperties {
|
||||||
|
unsigned int isVectorBased : 1;
|
||||||
|
unsigned int hasSliceInformation : 1;
|
||||||
|
unsigned int hasAlignmentInformation : 1;
|
||||||
|
unsigned int resizingMode : 2;
|
||||||
|
unsigned int templateRenderingMode : 3;
|
||||||
|
unsigned int exifOrientation : 4;
|
||||||
|
unsigned int isAlphaCropped : 1;
|
||||||
|
unsigned int isFlippable : 1;
|
||||||
|
unsigned int isTintable : 1;
|
||||||
|
unsigned int preservedVectorRepresentation : 1;
|
||||||
|
unsigned int _reserved : 16;
|
||||||
|
} _imageProperties;
|
||||||
|
double _scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property (readonly) CGRect NS_alignmentRect;
|
||||||
|
@property (nonatomic, readonly) NSEdgeInsets alignmentEdgeInsets;
|
||||||
|
@property (nonatomic, readonly) int blendMode;
|
||||||
|
@property (nonatomic, readonly) CGImageRef croppedImage;
|
||||||
|
@property (nonatomic, readonly) NSEdgeInsets edgeInsets;
|
||||||
|
@property (nonatomic, readonly) int exifOrientation;
|
||||||
|
@property (nonatomic, readonly) BOOL hasAlignmentInformation;
|
||||||
|
@property (nonatomic, readonly) BOOL hasSliceInformation;
|
||||||
|
@property (nonatomic, readonly) CGImageRef image;
|
||||||
|
@property (nonatomic, readonly) long long imageType;
|
||||||
|
@property (nonatomic, readonly) BOOL isAlphaCropped;
|
||||||
|
@property (nonatomic, readonly) BOOL isFlippable;
|
||||||
|
@property (nonatomic, readonly) BOOL isStructured;
|
||||||
|
@property (nonatomic, readonly) BOOL isTemplate;
|
||||||
|
@property (nonatomic, readonly) BOOL isVectorBased;
|
||||||
|
@property (nonatomic, readonly) double opacity;
|
||||||
|
@property (nonatomic, readonly) BOOL preservedVectorRepresentation;
|
||||||
|
@property (nonatomic, readonly) long long resizingMode;
|
||||||
|
@property (nonatomic, readonly) double scale;
|
||||||
|
@property (nonatomic, readonly) CGSize size;
|
||||||
|
@property (nonatomic, readonly) long long templateRenderingMode;
|
||||||
|
|
||||||
|
- (id)baseKey;
|
||||||
|
- (CGRect)alphaCroppedRect;
|
||||||
|
- (CGImageRef)createImageFromPDFRenditionWithScale:(double)arg1;
|
||||||
|
- (CGImageRef)croppedImage;
|
||||||
|
|
||||||
|
- (id)initWithName:(id)arg1 usingRenditionKey:(id)arg2 fromTheme:(unsigned long long)arg3;
|
||||||
|
|
||||||
|
- (CGSize)originalUncroppedSize;
|
||||||
|
- (double)positionOfSliceBoundary:(unsigned int)arg1;
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@class CUIRenditionKey;
|
||||||
|
|
||||||
|
@interface CUINamedLookup: NSObject <NSLocking> {
|
||||||
|
unsigned int _distilledInVersion;
|
||||||
|
CUIRenditionKey * _key;
|
||||||
|
NSString * _name;
|
||||||
|
unsigned int _odContent;
|
||||||
|
NSString * _signature;
|
||||||
|
unsigned long long _storageRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)initWithName:(id)arg1 usingRenditionKey:(id)arg2 fromTheme:(unsigned long long)arg3;
|
||||||
|
|
||||||
|
@end
|
||||||
1
PrivateFrameworks/CoreUI.framework/Versions/Current
Symbolic link
1
PrivateFrameworks/CoreUI.framework/Versions/Current
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
A
|
||||||
9
PrivateFrameworks/CoreUI.framework/module.modulemap
Normal file
9
PrivateFrameworks/CoreUI.framework/module.modulemap
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module CoreUI {
|
||||||
|
// umbrella header "CoreUI.h"
|
||||||
|
// Here is the list of your private headers.
|
||||||
|
header "Headers/CUICatalog.h"
|
||||||
|
header "Headers/CUINamedLookup.h"
|
||||||
|
header "Headers/CUINamedImage.h"
|
||||||
|
|
||||||
|
export *
|
||||||
|
}
|
||||||
@@ -6,17 +6,89 @@
|
|||||||
objectVersion = 77;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXBuildFile section */
|
||||||
54442BF42E378B71008A870E /* QLApps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QLApps.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
5405CF5C2EA1191A00613856 /* PreviewGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF5B2EA1191A00613856 /* PreviewGenerator.swift */; };
|
||||||
/* End PBXFileReference section */
|
5405CF5E2EA1199B00613856 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF5D2EA1199B00613856 /* Shared.swift */; };
|
||||||
|
5405CF652EA1376B00613856 /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF642EA1376B00613856 /* Zip.swift */; };
|
||||||
|
54442C232E378BAF008A870E /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54442C222E378BAF008A870E /* Quartz.framework */; };
|
||||||
|
54442C302E378BAF008A870E /* QLPreview.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 54442C202E378BAF008A870E /* QLPreview.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
54442C702E378BDD008A870E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54442C6A2E378BDD008A870E /* AppDelegate.swift */; };
|
||||||
|
54442C712E378BDD008A870E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 54442C6B2E378BDD008A870E /* Assets.xcassets */; };
|
||||||
|
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 */; };
|
||||||
|
545459C42EA469E4002892E5 /* defaultIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 54D3A6F22EA4603B001EF4F6 /* defaultIcon.png */; };
|
||||||
|
545459C52EA469EA002892E5 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 54D3A6F32EA4603B001EF4F6 /* template.html */; };
|
||||||
|
545459C72EA4773A002892E5 /* AppIcon+Car.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545459C62EA4773A002892E5 /* AppIcon+Car.swift */; };
|
||||||
|
545459C92EA47C37002892E5 /* Plist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545459C82EA47C37002892E5 /* Plist.swift */; };
|
||||||
|
5469E11D2EA5930C00D46CE7 /* Entitlements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5469E11C2EA5930C00D46CE7 /* Entitlements.swift */; };
|
||||||
|
54D3A6EC2EA31B52001EF4F6 /* AppCategories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D3A6EB2EA31B52001EF4F6 /* AppCategories.swift */; };
|
||||||
|
54D3A6EE2EA39CC6001EF4F6 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D3A6ED2EA39CC6001EF4F6 /* AppIcon.swift */; };
|
||||||
|
54D3A6F02EA3F49F001EF4F6 /* RoundedIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D3A6EF2EA3F49F001EF4F6 /* RoundedIcon.swift */; };
|
||||||
|
54D3A6F72EA46154001EF4F6 /* CoreUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54D3A6F52EA4610B001EF4F6 /* CoreUI.framework */; };
|
||||||
|
54D3A6FE2EA465B4001EF4F6 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54D3A6FA2EA46588001EF4F6 /* CoreGraphics.framework */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
54442BF62E378B71008A870E /* QLApps */ = {
|
54442C2E2E378BAF008A870E /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXContainerItemProxy;
|
||||||
path = QLApps;
|
containerPortal = 54442BEC2E378B71008A870E /* Project object */;
|
||||||
sourceTree = "<group>";
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 54442C1F2E378BAF008A870E;
|
||||||
|
remoteInfo = QLPreview;
|
||||||
};
|
};
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
54442C312E378BAF008A870E /* Embed Foundation Extensions */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
54442C302E378BAF008A870E /* QLPreview.appex in Embed Foundation Extensions */,
|
||||||
|
);
|
||||||
|
name = "Embed Foundation Extensions";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
5405CF5B2EA1191A00613856 /* PreviewGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewGenerator.swift; sourceTree = "<group>"; };
|
||||||
|
5405CF5D2EA1199B00613856 /* Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = "<group>"; };
|
||||||
|
5405CF642EA1376B00613856 /* Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zip.swift; sourceTree = "<group>"; };
|
||||||
|
54442BF42E378B71008A870E /* QLApps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QLApps.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
54442C202E378BAF008A870E /* QLPreview.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QLPreview.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
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>"; };
|
||||||
|
54442C6B2E378BDD008A870E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
54442C6C2E378BDD008A870E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||||
|
54442C6E2E378BDD008A870E /* QLApps.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QLApps.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>"; };
|
||||||
|
545459C62EA4773A002892E5 /* AppIcon+Car.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppIcon+Car.swift"; sourceTree = "<group>"; };
|
||||||
|
545459C82EA47C37002892E5 /* Plist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plist.swift; sourceTree = "<group>"; };
|
||||||
|
5469E11C2EA5930C00D46CE7 /* Entitlements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Entitlements.swift; sourceTree = "<group>"; };
|
||||||
|
5485EE362EB1460C009E3905 /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = System/Library/Frameworks/Network.framework; sourceTree = SDKROOT; };
|
||||||
|
54D3A6E52EA31B13001EF4F6 /* ThumbnailGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailGenerator.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 /* RoundedIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedIcon.swift; sourceTree = "<group>"; };
|
||||||
|
54D3A6F22EA4603B001EF4F6 /* defaultIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = defaultIcon.png; sourceTree = "<group>"; };
|
||||||
|
54D3A6F32EA4603B001EF4F6 /* template.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = template.html; sourceTree = "<group>"; };
|
||||||
|
54D3A6F52EA4610B001EF4F6 /* CoreUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = CoreUI.framework; sourceTree = "<group>"; };
|
||||||
|
54D3A6FA2EA46588001EF4F6 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
|
||||||
|
54D3A6FC2EA465A9001EF4F6 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };
|
||||||
|
54D3A6FF2EA465C4001EF4F6 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
|
||||||
|
54E0874C2EB1488300979D91 /* ApplicationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ApplicationServices.framework; path = System/Library/Frameworks/ApplicationServices.framework; sourceTree = SDKROOT; };
|
||||||
|
54E0874E2EB1489100979D91 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
|
||||||
|
54E087512EB148B700979D91 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
||||||
|
54E087532EB148DB00979D91 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; };
|
||||||
|
54E087552EB148DF00979D91 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
|
||||||
|
54E087572EB148E700979D91 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
54442BF12E378B71008A870E /* Frameworks */ = {
|
54442BF12E378B71008A870E /* Frameworks */ = {
|
||||||
@@ -26,13 +98,45 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
54442C1D2E378BAF008A870E /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
54D3A6FE2EA465B4001EF4F6 /* CoreGraphics.framework in Frameworks */,
|
||||||
|
54442C232E378BAF008A870E /* Quartz.framework in Frameworks */,
|
||||||
|
54D3A6F72EA46154001EF4F6 /* CoreUI.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
541051562E37AFC10083670B /* src */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
54D3A6EB2EA31B52001EF4F6 /* AppCategories.swift */,
|
||||||
|
54D3A6ED2EA39CC6001EF4F6 /* AppIcon.swift */,
|
||||||
|
545459C62EA4773A002892E5 /* AppIcon+Car.swift */,
|
||||||
|
54D3A6EF2EA3F49F001EF4F6 /* RoundedIcon.swift */,
|
||||||
|
5405CF5D2EA1199B00613856 /* Shared.swift */,
|
||||||
|
545459C82EA47C37002892E5 /* Plist.swift */,
|
||||||
|
5405CF642EA1376B00613856 /* Zip.swift */,
|
||||||
|
5469E11C2EA5930C00D46CE7 /* Entitlements.swift */,
|
||||||
|
54D3A6E52EA31B13001EF4F6 /* ThumbnailGenerator.swift */,
|
||||||
|
5405CF5B2EA1191A00613856 /* PreviewGenerator.swift */,
|
||||||
|
);
|
||||||
|
path = src;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
54442BEB2E378B71008A870E = {
|
54442BEB2E378B71008A870E = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
54442BF62E378B71008A870E /* QLApps */,
|
54D3A6F62EA4610B001EF4F6 /* PrivateFrameworks */,
|
||||||
|
54D3A6F42EA46069001EF4F6 /* resources */,
|
||||||
|
541051562E37AFC10083670B /* src */,
|
||||||
|
54442C6F2E378BDD008A870E /* QLApps */,
|
||||||
|
54442C782E378BE0008A870E /* QLPreview */,
|
||||||
|
54442C212E378BAF008A870E /* Frameworks */,
|
||||||
54442BF52E378B71008A870E /* Products */,
|
54442BF52E378B71008A870E /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -41,10 +145,68 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
54442BF42E378B71008A870E /* QLApps.app */,
|
54442BF42E378B71008A870E /* QLApps.app */,
|
||||||
|
54442C202E378BAF008A870E /* QLPreview.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
54442C212E378BAF008A870E /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
54E087572EB148E700979D91 /* WebKit.framework */,
|
||||||
|
54E087552EB148DF00979D91 /* Security.framework */,
|
||||||
|
54E087532EB148DB00979D91 /* QuickLook.framework */,
|
||||||
|
54E087512EB148B700979D91 /* libz.tbd */,
|
||||||
|
54E0874E2EB1489100979D91 /* CoreFoundation.framework */,
|
||||||
|
54E0874C2EB1488300979D91 /* ApplicationServices.framework */,
|
||||||
|
5485EE362EB1460C009E3905 /* Network.framework */,
|
||||||
|
54D3A6FF2EA465C4001EF4F6 /* AppKit.framework */,
|
||||||
|
54D3A6FC2EA465A9001EF4F6 /* CoreServices.framework */,
|
||||||
|
54D3A6FA2EA46588001EF4F6 /* CoreGraphics.framework */,
|
||||||
|
54442C222E378BAF008A870E /* Quartz.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
54442C6F2E378BDD008A870E /* QLApps */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
54442C6A2E378BDD008A870E /* AppDelegate.swift */,
|
||||||
|
54442C6B2E378BDD008A870E /* Assets.xcassets */,
|
||||||
|
54442C6D2E378BDD008A870E /* MainMenu.xib */,
|
||||||
|
54442C6E2E378BDD008A870E /* QLApps.entitlements */,
|
||||||
|
);
|
||||||
|
path = QLApps;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
54442C782E378BE0008A870E /* QLPreview */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
54442C732E378BE0008A870E /* Info.plist */,
|
||||||
|
54442C742E378BE0008A870E /* PreviewViewController.swift */,
|
||||||
|
54442C762E378BE0008A870E /* PreviewViewController.xib */,
|
||||||
|
54442C772E378BE0008A870E /* QLPreview.entitlements */,
|
||||||
|
);
|
||||||
|
path = QLPreview;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
54D3A6F42EA46069001EF4F6 /* resources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
54D3A6F22EA4603B001EF4F6 /* defaultIcon.png */,
|
||||||
|
54D3A6F32EA4603B001EF4F6 /* template.html */,
|
||||||
|
);
|
||||||
|
path = resources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
54D3A6F62EA4610B001EF4F6 /* PrivateFrameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
54D3A6F52EA4610B001EF4F6 /* CoreUI.framework */,
|
||||||
|
);
|
||||||
|
path = PrivateFrameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -55,13 +217,12 @@
|
|||||||
54442BF02E378B71008A870E /* Sources */,
|
54442BF02E378B71008A870E /* Sources */,
|
||||||
54442BF12E378B71008A870E /* Frameworks */,
|
54442BF12E378B71008A870E /* Frameworks */,
|
||||||
54442BF22E378B71008A870E /* Resources */,
|
54442BF22E378B71008A870E /* Resources */,
|
||||||
|
54442C312E378BAF008A870E /* Embed Foundation Extensions */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
54442C2F2E378BAF008A870E /* PBXTargetDependency */,
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
54442BF62E378B71008A870E /* QLApps */,
|
|
||||||
);
|
);
|
||||||
name = QLApps;
|
name = QLApps;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
@@ -70,6 +231,25 @@
|
|||||||
productReference = 54442BF42E378B71008A870E /* QLApps.app */;
|
productReference = 54442BF42E378B71008A870E /* QLApps.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
54442C1F2E378BAF008A870E /* QLPreview */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 54442C352E378BAF008A870E /* Build configuration list for PBXNativeTarget "QLPreview" */;
|
||||||
|
buildPhases = (
|
||||||
|
54442C1C2E378BAF008A870E /* Sources */,
|
||||||
|
54442C1D2E378BAF008A870E /* Frameworks */,
|
||||||
|
54442C1E2E378BAF008A870E /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = QLPreview;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = QLPreview;
|
||||||
|
productReference = 54442C202E378BAF008A870E /* QLPreview.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@@ -83,6 +263,9 @@
|
|||||||
54442BF32E378B71008A870E = {
|
54442BF32E378B71008A870E = {
|
||||||
CreatedOnToolsVersion = 16.4;
|
CreatedOnToolsVersion = 16.4;
|
||||||
};
|
};
|
||||||
|
54442C1F2E378BAF008A870E = {
|
||||||
|
CreatedOnToolsVersion = 16.4;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = 54442BEF2E378B71008A870E /* Build configuration list for PBXProject "QLApps" */;
|
buildConfigurationList = 54442BEF2E378B71008A870E /* Build configuration list for PBXProject "QLApps" */;
|
||||||
@@ -100,6 +283,7 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
54442BF32E378B71008A870E /* QLApps */,
|
54442BF32E378B71008A870E /* QLApps */,
|
||||||
|
54442C1F2E378BAF008A870E /* QLPreview */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -109,6 +293,18 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
54442C712E378BDD008A870E /* Assets.xcassets in Resources */,
|
||||||
|
54442C722E378BDD008A870E /* MainMenu.xib in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
54442C1E2E378BAF008A870E /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
545459C42EA469E4002892E5 /* defaultIcon.png in Resources */,
|
||||||
|
545459C52EA469EA002892E5 /* template.html in Resources */,
|
||||||
|
54442C7B2E378BE0008A870E /* PreviewViewController.xib in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -119,11 +315,56 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
54442C702E378BDD008A870E /* AppDelegate.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
54442C1C2E378BAF008A870E /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
54D3A6F02EA3F49F001EF4F6 /* RoundedIcon.swift in Sources */,
|
||||||
|
5469E11D2EA5930C00D46CE7 /* Entitlements.swift in Sources */,
|
||||||
|
54442C792E378BE0008A870E /* PreviewViewController.swift in Sources */,
|
||||||
|
5405CF5C2EA1191A00613856 /* PreviewGenerator.swift in Sources */,
|
||||||
|
54D3A6EE2EA39CC6001EF4F6 /* AppIcon.swift in Sources */,
|
||||||
|
545459C92EA47C37002892E5 /* Plist.swift in Sources */,
|
||||||
|
5405CF5E2EA1199B00613856 /* Shared.swift in Sources */,
|
||||||
|
545459C72EA4773A002892E5 /* AppIcon+Car.swift in Sources */,
|
||||||
|
54D3A6EC2EA31B52001EF4F6 /* AppCategories.swift in Sources */,
|
||||||
|
5405CF652EA1376B00613856 /* Zip.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
54442C2F2E378BAF008A870E /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 54442C1F2E378BAF008A870E /* QLPreview */;
|
||||||
|
targetProxy = 54442C2E2E378BAF008A870E /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
54442C6D2E378BDD008A870E /* MainMenu.xib */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
54442C6C2E378BDD008A870E /* Base */,
|
||||||
|
);
|
||||||
|
name = MainMenu.xib;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
54442C762E378BE0008A870E /* PreviewViewController.xib */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
54442C752E378BE0008A870E /* Base */,
|
||||||
|
);
|
||||||
|
name = PreviewViewController.xib;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
54442BFF2E378B71008A870E /* Debug */ = {
|
54442BFF2E378B71008A870E /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
@@ -164,6 +405,7 @@
|
|||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/PrivateFrameworks";
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
@@ -179,13 +421,17 @@
|
|||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
"$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -228,8 +474,10 @@
|
|||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/PrivateFrameworks";
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = "";
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
@@ -237,11 +485,15 @@
|
|||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
"$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -251,10 +503,12 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = QLApps/QLApps.entitlements;
|
CODE_SIGN_ENTITLEMENTS = QLApps/QLApps.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 641;
|
||||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = UY657LKNHJ;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
@@ -267,6 +521,7 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLApps;
|
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLApps;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -279,10 +534,12 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = QLApps/QLApps.entitlements;
|
CODE_SIGN_ENTITLEMENTS = QLApps/QLApps.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 641;
|
||||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = UY657LKNHJ;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
@@ -295,12 +552,71 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLApps;
|
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLApps;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
54442C322E378BAF008A870E /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_ENTITLEMENTS = QLPreview/QLPreview.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
CURRENT_PROJECT_VERSION = 641;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = UY657LKNHJ;
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = QLPreview/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = QLPreview;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
"@executable_path/../../../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLApps.QLPreview;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_INCLUDE_PATHS = "$(inherited) $(SRCROOT)/PrivateFrameworks/CoreUI.framework";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
54442C332E378BAF008A870E /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_ENTITLEMENTS = QLPreview/QLPreview.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
CURRENT_PROJECT_VERSION = 641;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = UY657LKNHJ;
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = QLPreview/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = QLPreview;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
"@executable_path/../../../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLApps.QLPreview;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_INCLUDE_PATHS = "$(inherited) $(SRCROOT)/PrivateFrameworks/CoreUI.framework";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@@ -322,6 +638,15 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
54442C352E378BAF008A870E /* Build configuration list for PBXNativeTarget "QLPreview" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
54442C322E378BAF008A870E /* Debug */,
|
||||||
|
54442C332E378BAF008A870E /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
};
|
};
|
||||||
rootObject = 54442BEC2E378B71008A870E /* Project object */;
|
rootObject = 54442BEC2E378B71008A870E /* Project object */;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
78
QLApps.xcodeproj/xcshareddata/xcschemes/QLApps.xcscheme
Normal file
78
QLApps.xcodeproj/xcshareddata/xcschemes/QLApps.xcscheme
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1640"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "54442BF32E378B71008A870E"
|
||||||
|
BuildableName = "QLApps.app"
|
||||||
|
BlueprintName = "QLApps"
|
||||||
|
ReferencedContainer = "container:QLApps.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "54442BF32E378B71008A870E"
|
||||||
|
BuildableName = "QLApps.app"
|
||||||
|
BlueprintName = "QLApps"
|
||||||
|
ReferencedContainer = "container:QLApps.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "54442BF32E378B71008A870E"
|
||||||
|
BuildableName = "QLApps.app"
|
||||||
|
BlueprintName = "QLApps"
|
||||||
|
ReferencedContainer = "container:QLApps.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
97
QLApps.xcodeproj/xcshareddata/xcschemes/QLPreview.xcscheme
Normal file
97
QLApps.xcodeproj/xcshareddata/xcschemes/QLPreview.xcscheme
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1640"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "54442C1F2E378BAF008A870E"
|
||||||
|
BuildableName = "QLPreview.appex"
|
||||||
|
BlueprintName = "QLPreview"
|
||||||
|
ReferencedContainer = "container:QLApps.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "54442BF32E378B71008A870E"
|
||||||
|
BuildableName = "QLApps.app"
|
||||||
|
BlueprintName = "QLApps"
|
||||||
|
ReferencedContainer = "container:QLApps.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "54442BF32E378B71008A870E"
|
||||||
|
BuildableName = "QLApps.app"
|
||||||
|
BlueprintName = "QLApps"
|
||||||
|
ReferencedContainer = "container:QLApps.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "54442BF32E378B71008A870E"
|
||||||
|
BuildableName = "QLApps.app"
|
||||||
|
BlueprintName = "QLApps"
|
||||||
|
ReferencedContainer = "container:QLApps.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
//
|
|
||||||
// AppDelegate.swift
|
|
||||||
// QLApps
|
|
||||||
//
|
|
||||||
// Created by - on 28.07.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
@main
|
@main
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
@IBOutlet var window: NSWindow!
|
@IBOutlet var window: NSWindow!
|
||||||
|
|
||||||
|
|
||||||
@@ -24,7 +16,5 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
QLPreview/Base.lproj/PreviewViewController.xib
Normal file
22
QLPreview/Base.lproj/PreviewViewController.xib
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23727" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="macosx"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23727"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<customObject id="-2" userLabel="File's Owner" customClass="PreviewViewController" customModule="QLPreview" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="view" destination="c22-O7-iKe" id="NRM-P4-wb6"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
|
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||||
|
<customView id="c22-O7-iKe" userLabel="Preview View">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="480" height="272"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<point key="canvasLocation" x="53" y="-36"/>
|
||||||
|
</customView>
|
||||||
|
</objects>
|
||||||
|
</document>
|
||||||
26
QLPreview/Info.plist
Normal file
26
QLPreview/Info.plist
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?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>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionAttributes</key>
|
||||||
|
<dict>
|
||||||
|
<key>QLIsDataBasedPreview</key>
|
||||||
|
<false/>
|
||||||
|
<key>QLSupportedContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>com.apple.itunes.ipa</string>
|
||||||
|
<string>com.apple.application-and-system-extension</string>
|
||||||
|
<string>com.apple.xcode.archive</string>
|
||||||
|
</array>
|
||||||
|
<key>QLSupportsSearchableItems</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.quicklook.preview</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).PreviewViewController</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
25
QLPreview/PreviewViewController.swift
Normal file
25
QLPreview/PreviewViewController.swift
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Cocoa
|
||||||
|
import Quartz // QLPreviewingController
|
||||||
|
import WebKit // WebView
|
||||||
|
import os // OSLog
|
||||||
|
|
||||||
|
// show Console logs with subsystem:de.relikd.QLApps
|
||||||
|
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "main")
|
||||||
|
|
||||||
|
class PreviewViewController: NSViewController, QLPreviewingController {
|
||||||
|
|
||||||
|
override var nibName: NSNib.Name? {
|
||||||
|
return NSNib.Name("PreviewViewController")
|
||||||
|
}
|
||||||
|
|
||||||
|
func preparePreviewOfFile(at url: URL) async throws {
|
||||||
|
let html = generateHtml(at: url)
|
||||||
|
// sure, we could use `WKWebView`, but that requires the `com.apple.security.network.client` entitlement
|
||||||
|
//let web = WKWebView(frame: self.view.bounds)
|
||||||
|
let web = WebView(frame: self.view.bounds)
|
||||||
|
web.autoresizingMask = [.width, .height]
|
||||||
|
self.view.addSubview(web)
|
||||||
|
web.mainFrame.loadHTMLString(html, baseURL: nil) // WebView
|
||||||
|
//web.loadHTMLString(html, baseURL: nil) // WKWebView
|
||||||
|
}
|
||||||
|
}
|
||||||
10
QLPreview/QLPreview.entitlements
Normal file
10
QLPreview/QLPreview.entitlements
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?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>
|
||||||
BIN
resources/defaultIcon.png
Normal file
BIN
resources/defaultIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
215
resources/template.html
Normal file
215
resources/template.html
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title></title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: #f7f7f7;
|
||||||
|
color: #000;
|
||||||
|
margin: 20px;
|
||||||
|
font: 13px monospace;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hiddenDiv {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app img {
|
||||||
|
-webkit-filter: drop-shadow(0px 0px 3px rgba(0,0,0,0.5));
|
||||||
|
filter: drop-shadow(0px 0px 3px rgba(0,0,0,0.5));
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info .subsection {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info .list {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info > .list {
|
||||||
|
display: inline-block;
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floatLeft {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { color: #aaa; }
|
||||||
|
a:hover { color: #000; }
|
||||||
|
a:visited { color: #aaa; }
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 4ex;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired, .warning {
|
||||||
|
color: darkred;
|
||||||
|
}
|
||||||
|
.expiring {
|
||||||
|
color: #996600;
|
||||||
|
}
|
||||||
|
.valid {
|
||||||
|
color: darkgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceType {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(odd) {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background: #323232;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { color: #aaa; }
|
||||||
|
a:hover { color: #fff; }
|
||||||
|
a:visited { color: #aaa; }
|
||||||
|
|
||||||
|
.expired, .warning {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.expiring {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
.valid {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(odd) {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: #292929;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app __AppInfoHidden__">
|
||||||
|
<h1>__AppInfoTitle__</h1>
|
||||||
|
<div class="floatLeft icon"><img alt="App icon" src="data:image/png;base64,__AppIcon__"/></div>
|
||||||
|
<div class="floatLeft info">
|
||||||
|
Name: <strong>__CFBundleName__</strong><br />
|
||||||
|
Version: __CFBundleShortVersionString__ (__CFBundleVersion__)<br />
|
||||||
|
BundleId: __CFBundleIdentifier__<br />
|
||||||
|
<div class="__ExtensionTypeHidden__">
|
||||||
|
Extension type: __ExtensionType__<br />
|
||||||
|
</div>
|
||||||
|
DeviceFamily: __UIDeviceFamily__<br />
|
||||||
|
SDK: __DTSDKName__<br />
|
||||||
|
Minimum OS Version: __MinimumOSVersion__<br />
|
||||||
|
</div>
|
||||||
|
<br class="clear" />
|
||||||
|
<h2>App Transport Security</h2>
|
||||||
|
__AppTransportSecurityFormatted__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="__ProvisionHidden__">
|
||||||
|
<div class="__AppInfoHidden__">
|
||||||
|
<h2>Provisioning</h2>
|
||||||
|
Profile name: <strong>__ProfileName__</strong><br />
|
||||||
|
</div>
|
||||||
|
<div class="__ProvisionTitleHidden__">
|
||||||
|
<h1><span class="__ExpStatus__">__ProfileName__</span></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Profile UUID: __ProfileUUID__<br />
|
||||||
|
Profile Type: __ProfilePlatform__ __ProfileType__<br />
|
||||||
|
Team: __TeamName__ (__TeamIds__)<br />
|
||||||
|
Creation date: __CreationDateFormatted__<br />
|
||||||
|
Expiration Date: <strong><span class="__ExpStatus__">__ExpirationDateFormatted__</span></strong><br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Entitlements</h2>
|
||||||
|
<div class="__EntitlementsWarningHidden__ warning">
|
||||||
|
<strong>Entitlements extraction failed.</strong>
|
||||||
|
</div>
|
||||||
|
__EntitlementsFormatted__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="__ProvisionHidden__">
|
||||||
|
<h2>Developer Certificates</h2>
|
||||||
|
__DeveloperCertificatesFormatted__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="__ProvisionHidden__">
|
||||||
|
<h2>Devices (__ProvisionedDevicesCount__)</h2>
|
||||||
|
__ProvisionedDevicesFormatted__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="__iTunesHidden__">
|
||||||
|
<h2>iTunes Metadata</h2>
|
||||||
|
iTunesId: __iTunesId__<br />
|
||||||
|
Title: __iTunesName__<br />
|
||||||
|
Genres: __iTunesGenres__<br />
|
||||||
|
Released: __iTunesReleaseDate__<br />
|
||||||
|
<br />
|
||||||
|
AppleId: __iTunesAppleId__<br />
|
||||||
|
Purchased: __iTunesPurchaseDate__<br />
|
||||||
|
Price: __iTunesPrice__<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>File info</h2>
|
||||||
|
__FileName__<br />
|
||||||
|
__FileInfo__<br />
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>ProvisionQL v__BundleShortVersionString__ (__BundleVersion__) __DEBUG__ (<a href="https://github.com/ealeksandrov/ProvisionQL">Fork on GitHub</a>)</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
156
src/AppCategories.swift
Normal file
156
src/AppCategories.swift
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/*
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# download: https://itunes.apple.com/WebObjects/MZStoreServices.woa/ws/genres
|
||||||
|
import json
|
||||||
|
ids = {}
|
||||||
|
|
||||||
|
def fn(data):
|
||||||
|
for k, v in data.items():
|
||||||
|
ids[k] = v['name']
|
||||||
|
if 'subgenres' in v:
|
||||||
|
fn(v['subgenres'])
|
||||||
|
|
||||||
|
with open('genres.json', 'r') as fp:
|
||||||
|
for cat in json.load(fp).values():
|
||||||
|
if 'App Store' in cat['name']:
|
||||||
|
fn(cat['subgenres'])
|
||||||
|
|
||||||
|
print(',\n'.join(f'{k}: "{v}"' for k, v in ids.items()))
|
||||||
|
print(len(ids))
|
||||||
|
*/
|
||||||
|
|
||||||
|
let AppCategories: [Int: String] = [
|
||||||
|
// MARK: iOS
|
||||||
|
6018: "Books",
|
||||||
|
6000: "Business",
|
||||||
|
6022: "Catalogs",
|
||||||
|
6026: "Developer Tools",
|
||||||
|
6017: "Education",
|
||||||
|
6016: "Entertainment",
|
||||||
|
6015: "Finance",
|
||||||
|
6023: "Food & Drink",
|
||||||
|
6014: "Games",
|
||||||
|
7001: "Action",
|
||||||
|
7002: "Adventure",
|
||||||
|
7004: "Board",
|
||||||
|
7005: "Card",
|
||||||
|
7006: "Casino",
|
||||||
|
7003: "Casual",
|
||||||
|
7007: "Dice",
|
||||||
|
7008: "Educational",
|
||||||
|
7009: "Family",
|
||||||
|
7011: "Music",
|
||||||
|
7012: "Puzzle",
|
||||||
|
7013: "Racing",
|
||||||
|
7014: "Role Playing",
|
||||||
|
7015: "Simulation",
|
||||||
|
7016: "Sports",
|
||||||
|
7017: "Strategy",
|
||||||
|
7018: "Trivia",
|
||||||
|
7019: "Word",
|
||||||
|
6027: "Graphics & Design",
|
||||||
|
6013: "Health & Fitness",
|
||||||
|
6012: "Lifestyle",
|
||||||
|
6021: "Magazines & Newspapers",
|
||||||
|
13007: "Arts & Photography",
|
||||||
|
13006: "Automotive",
|
||||||
|
13008: "Brides & Weddings",
|
||||||
|
13009: "Business & Investing",
|
||||||
|
13010: "Children's Magazines",
|
||||||
|
13011: "Computers & Internet",
|
||||||
|
13012: "Cooking, Food & Drink",
|
||||||
|
13013: "Crafts & Hobbies",
|
||||||
|
13014: "Electronics & Audio",
|
||||||
|
13015: "Entertainment",
|
||||||
|
13002: "Fashion & Style",
|
||||||
|
13017: "Health, Mind & Body",
|
||||||
|
13018: "History",
|
||||||
|
13003: "Home & Garden",
|
||||||
|
13019: "Literary Magazines & Journals",
|
||||||
|
13020: "Men's Interest",
|
||||||
|
13021: "Movies & Music",
|
||||||
|
13001: "News & Politics",
|
||||||
|
13004: "Outdoors & Nature",
|
||||||
|
13023: "Parenting & Family",
|
||||||
|
13024: "Pets",
|
||||||
|
13025: "Professional & Trade",
|
||||||
|
13026: "Regional News",
|
||||||
|
13027: "Science",
|
||||||
|
13005: "Sports & Leisure",
|
||||||
|
13028: "Teens",
|
||||||
|
13029: "Travel & Regional",
|
||||||
|
13030: "Women's Interest",
|
||||||
|
6020: "Medical",
|
||||||
|
6011: "Music",
|
||||||
|
6010: "Navigation",
|
||||||
|
6009: "News",
|
||||||
|
6008: "Photo & Video",
|
||||||
|
6007: "Productivity",
|
||||||
|
6006: "Reference",
|
||||||
|
6024: "Shopping",
|
||||||
|
6005: "Social Networking",
|
||||||
|
6004: "Sports",
|
||||||
|
6025: "Stickers",
|
||||||
|
16003: "Animals & Nature",
|
||||||
|
16005: "Art",
|
||||||
|
16006: "Celebrations",
|
||||||
|
16007: "Celebrities",
|
||||||
|
16008: "Comics & Cartoons",
|
||||||
|
16009: "Eating & Drinking",
|
||||||
|
16001: "Emoji & Expressions",
|
||||||
|
16026: "Fashion",
|
||||||
|
16010: "Gaming",
|
||||||
|
16025: "Kids & Family",
|
||||||
|
16014: "Movies & TV",
|
||||||
|
16015: "Music",
|
||||||
|
16017: "People",
|
||||||
|
16019: "Places & Objects",
|
||||||
|
16021: "Sports & Activities",
|
||||||
|
6003: "Travel",
|
||||||
|
6002: "Utilities",
|
||||||
|
6001: "Weather",
|
||||||
|
|
||||||
|
// MARK: macOS
|
||||||
|
12001: "Business",
|
||||||
|
12002: "Developer Tools",
|
||||||
|
12003: "Education",
|
||||||
|
12004: "Entertainment",
|
||||||
|
12005: "Finance",
|
||||||
|
12006: "Games",
|
||||||
|
12201: "Action",
|
||||||
|
12202: "Adventure",
|
||||||
|
12204: "Board",
|
||||||
|
12205: "Card",
|
||||||
|
12206: "Casino",
|
||||||
|
12203: "Casual",
|
||||||
|
12207: "Dice",
|
||||||
|
12208: "Educational",
|
||||||
|
12209: "Family",
|
||||||
|
12210: "Kids",
|
||||||
|
12211: "Music",
|
||||||
|
12212: "Puzzle",
|
||||||
|
12213: "Racing",
|
||||||
|
12214: "Role Playing",
|
||||||
|
12215: "Simulation",
|
||||||
|
12216: "Sports",
|
||||||
|
12217: "Strategy",
|
||||||
|
12218: "Trivia",
|
||||||
|
12219: "Word",
|
||||||
|
12022: "Graphics & Design",
|
||||||
|
12007: "Health & Fitness",
|
||||||
|
12008: "Lifestyle",
|
||||||
|
12010: "Medical",
|
||||||
|
12011: "Music",
|
||||||
|
12012: "News",
|
||||||
|
12013: "Photography",
|
||||||
|
12014: "Productivity",
|
||||||
|
12015: "Reference",
|
||||||
|
12016: "Social Networking",
|
||||||
|
12017: "Sports",
|
||||||
|
12018: "Travel",
|
||||||
|
12019: "Utilities",
|
||||||
|
12020: "Video",
|
||||||
|
12021: "Weather",
|
||||||
|
]
|
||||||
91
src/AppIcon+Car.swift
Normal file
91
src/AppIcon+Car.swift
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import Foundation
|
||||||
|
import AppKit // NSImage
|
||||||
|
import CoreUI // CUICatalog
|
||||||
|
import os // OSLog
|
||||||
|
|
||||||
|
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "AppIcon+Car")
|
||||||
|
|
||||||
|
// this has been written from scratch but general usage on
|
||||||
|
// including the private framework has been taken from:
|
||||||
|
// https://github.com/showxu/cartools
|
||||||
|
// also see:
|
||||||
|
// https://blog.timac.org/2018/1018-reverse-engineering-the-car-file-format/
|
||||||
|
|
||||||
|
extension AppIcon {
|
||||||
|
/// Use `CUICatalog` to extract an image from `Assets.car`
|
||||||
|
func imageFromAssetsCar(_ imageName: String) -> NSImage? {
|
||||||
|
guard let data = meta.readPayloadFile("Assets.car") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let catalog: CUICatalog
|
||||||
|
do {
|
||||||
|
catalog = try data.withUnsafeBytes { try CUICatalog(bytes: $0.baseAddress!, length: UInt64(data.count)) }
|
||||||
|
} catch {
|
||||||
|
os_log(.error, log: log, "[icon-car] ERROR: could not open catalog: %{public}@", error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let validName = carVerifyNameExists(imageName, in: catalog) {
|
||||||
|
if let bestImage = carFindHighestResolutionIcon(catalog.images(withName: validName)) {
|
||||||
|
os_log(.debug, log: log, "[icon-car] using Assets.car with key %{public}@", validName)
|
||||||
|
return NSImage(cgImage: bestImage.image, size: bestImage.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Helper: Assets.car
|
||||||
|
|
||||||
|
/// Helper method to check available icon names. Will return a valid name or `nil` if no image with that key is found.
|
||||||
|
func carVerifyNameExists(_ imageName: String, in catalog: CUICatalog) -> String? {
|
||||||
|
if let availableNames = catalog.allImageNames(), !availableNames.contains(imageName) {
|
||||||
|
// Theoretically this should never happen. Assuming the image name is found in an image file.
|
||||||
|
os_log(.info, log: log, "[icon-car] WARN: key '%{public}@' does not match any available key", imageName)
|
||||||
|
|
||||||
|
if let alternativeName = carSearchAlternativeName(imageName, inAvailable: availableNames) {
|
||||||
|
os_log(.info, log: log, "[icon-car] falling back to '%{public}@'", alternativeName)
|
||||||
|
return alternativeName
|
||||||
|
}
|
||||||
|
os_log(.debug, log: log, "[icon-car] available keys: %{public}@", catalog.allImageNames() ?? [])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return imageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If exact name does not exist in catalog, search for a name that shares the same prefix.
|
||||||
|
/// E.g., "AppIcon60x60" may match "AppIcon" or "AppIcon60x60_small"
|
||||||
|
func carSearchAlternativeName(_ originalName: String, inAvailable availableNames: [String]) -> String? {
|
||||||
|
var bestOption: String? = nil
|
||||||
|
var bestDiff: Int = 999
|
||||||
|
|
||||||
|
for option in availableNames {
|
||||||
|
if option.hasPrefix(originalName) || originalName.hasPrefix(option) {
|
||||||
|
let thisDiff = max(originalName.count, option.count) - min(originalName.count, option.count)
|
||||||
|
if thisDiff < bestDiff {
|
||||||
|
bestDiff = thisDiff
|
||||||
|
bestOption = option
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestOption
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a list of `CUINamedImage`, return the one with the highest resolution. Vector graphics are ignored.
|
||||||
|
func carFindHighestResolutionIcon(_ availableImages: [CUINamedImage]) -> CUINamedImage? {
|
||||||
|
var largestWidth: CGFloat = 0
|
||||||
|
var largestImage: CUINamedImage? = nil
|
||||||
|
// cast to NSArray is necessary as otherwise this will crash
|
||||||
|
for img in availableImages as NSArray {
|
||||||
|
guard let img = img as? CUINamedImage else {
|
||||||
|
continue // ignore CUINamedMultisizeImageSet
|
||||||
|
}
|
||||||
|
let w = img.size.width
|
||||||
|
if w > largestWidth {
|
||||||
|
largestWidth = w
|
||||||
|
largestImage = img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return largestImage
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/AppIcon.swift
Normal file
105
src/AppIcon.swift
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import Foundation
|
||||||
|
import AppKit // NSImage
|
||||||
|
import os // OSLog
|
||||||
|
|
||||||
|
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "AppIcon")
|
||||||
|
|
||||||
|
|
||||||
|
struct AppIcon {
|
||||||
|
let meta: QuickLookInfo
|
||||||
|
|
||||||
|
init(_ meta: QuickLookInfo) {
|
||||||
|
self.meta = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
// 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)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract image name from app plist
|
||||||
|
var plistImgNames = iconNamesFromPlist(appPlist)
|
||||||
|
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")
|
||||||
|
|
||||||
|
// First, try if an image file with that name exists.
|
||||||
|
if let actualName = expandImageName(plistImgNames) {
|
||||||
|
os_log(.debug, log: log, "[icon] using plist image file %{public}@", actualName)
|
||||||
|
if meta.type == .IPA {
|
||||||
|
let data = meta.zipFile!.unzipFile(actualName)!
|
||||||
|
return NSImage(data: data)!
|
||||||
|
}
|
||||||
|
return NSImage(contentsOfFile: actualName)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else: try Assets.car
|
||||||
|
if let img = imageFromAssetsCar(plistImgNames.first!) {
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to default icon
|
||||||
|
let iconURL = Bundle.main.url(forResource: "defaultIcon", withExtension: "png")!
|
||||||
|
return NSImage(contentsOf: iconURL)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Extension: NSImage
|
||||||
|
|
||||||
|
// AppIcon extension
|
||||||
|
extension NSImage {
|
||||||
|
/// Because some (PNG) image data will return weird float values
|
||||||
|
private func bestImageSize() -> NSSize {
|
||||||
|
var w: Int = 0
|
||||||
|
var h: Int = 0
|
||||||
|
for imageRep in self.representations {
|
||||||
|
w = max(w, imageRep.pixelsWide)
|
||||||
|
h = max(h, imageRep.pixelsHigh)
|
||||||
|
}
|
||||||
|
return NSSize(width: w, height: h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply rounded corners to image (iOS7 style)
|
||||||
|
func withRoundCorners() -> NSImage {
|
||||||
|
let existingSize = bestImageSize()
|
||||||
|
let composedImage = NSImage(size: existingSize)
|
||||||
|
|
||||||
|
composedImage.lockFocus()
|
||||||
|
NSGraphicsContext.current?.imageInterpolation = .high
|
||||||
|
|
||||||
|
let imageFrame = NSRect(origin: .zero, size: existingSize)
|
||||||
|
let clipPath = NSBezierPath.IOS7RoundedRect(imageFrame, cornerRadius: existingSize.width * 0.225)
|
||||||
|
clipPath.windingRule = .evenOdd
|
||||||
|
clipPath.addClip()
|
||||||
|
|
||||||
|
self.draw(in: imageFrame)
|
||||||
|
composedImage.unlockFocus()
|
||||||
|
return composedImage
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: [:])!
|
||||||
|
return imageDataPNG.base64EncodedString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the image is larger than the provided maximum size, scale it down. Otherwise leave it untouched.
|
||||||
|
// func downscale(ifLargerThan maxSize: CGSize) {
|
||||||
|
// // TODO: if downscale, then this should respect retina resolution
|
||||||
|
// if size.width > maxSize.width && size.height > maxSize.height {
|
||||||
|
// self.size = maxSize
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
143
src/Entitlements.swift
Normal file
143
src/Entitlements.swift
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import Foundation
|
||||||
|
import os // OSLog
|
||||||
|
|
||||||
|
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Entitlements")
|
||||||
|
|
||||||
|
|
||||||
|
struct Entitlements {
|
||||||
|
var hasError: Bool = false
|
||||||
|
/// only set after calling `applyFallbackIfNeeded(:)`
|
||||||
|
var html: String? = nil
|
||||||
|
|
||||||
|
private let binaryPath: String
|
||||||
|
/// It is either `plist` or `codeSignErrors` not both.
|
||||||
|
private var plist: [String: Any]? = nil
|
||||||
|
/// It is either `plist` or `codeSignErrors` not both.
|
||||||
|
private var codeSignError: String? = nil
|
||||||
|
|
||||||
|
/// Use provision plist data without running `codesign` or
|
||||||
|
static func withoutBinary() -> Self {
|
||||||
|
return Entitlements(forBinary: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// First, try to extract real entitlements by running `SecCode` module in-memory.
|
||||||
|
/// If that fails, fallback to running `codesign` via system call.
|
||||||
|
init(forBinary path: String?) {
|
||||||
|
guard let path else {
|
||||||
|
self.binaryPath = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.binaryPath = path
|
||||||
|
if FileManager.default.fileExists(atPath: path) {
|
||||||
|
self.plist = getSecCodeEntitlements()
|
||||||
|
} else {
|
||||||
|
os_log(.error, log: log, "[entitlements] provided binary '%{public}@' does not exist (unzip error?)", path)
|
||||||
|
self.plist = nil
|
||||||
|
self.codeSignError = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - public methods
|
||||||
|
|
||||||
|
/// Provided provision plist is only used if @c SecCode and @c CodeSign failed.
|
||||||
|
mutating func applyFallbackIfNeeded(_ fallbackEntitlementsPlist: PlistDict?) {
|
||||||
|
// checking for !error ensures that codesign gets precedence.
|
||||||
|
// show error before falling back to provision based entitlements.
|
||||||
|
if plist == nil && codeSignError == nil {
|
||||||
|
if let fallbackEntitlementsPlist {
|
||||||
|
os_log(.debug, log: log, "[entitlements] fallback to provision plist entitlements")
|
||||||
|
self.plist = fallbackEntitlementsPlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.html = format(plist)
|
||||||
|
self.plist = nil // free memory
|
||||||
|
self.codeSignError = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print formatted plist in a @c \<pre> tag
|
||||||
|
func format(_ plist: [String: Any]?) -> String? {
|
||||||
|
guard let plist else {
|
||||||
|
return codeSignError // may be nil
|
||||||
|
}
|
||||||
|
var output = ""
|
||||||
|
recursiveKeyValue(plist, &output)
|
||||||
|
return "<pre>\(output)</pre>"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SecCode in-memory reader
|
||||||
|
|
||||||
|
/// use in-memory `SecCode` for entitlement extraction
|
||||||
|
func getSecCodeEntitlements() -> PlistDict? {
|
||||||
|
let url = URL(fileURLWithPath: self.binaryPath)
|
||||||
|
var codeRef: SecStaticCode?
|
||||||
|
SecStaticCodeCreateWithPath(url as CFURL, [], &codeRef)
|
||||||
|
guard let codeRef else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var requirementInfo: CFDictionary?
|
||||||
|
SecCodeCopySigningInformation(codeRef, SecCSFlags(rawValue: kSecCSRequirementInformation), &requirementInfo)
|
||||||
|
guard let requirementInfo = requirementInfo as? PlistDict else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// expect magic number header. Currently no support for other formats.
|
||||||
|
let header = data.subdata(in: 0..<4)
|
||||||
|
guard header == Data([0xFA, 0xDE, 0x71, 0x71]) else {
|
||||||
|
os_log(.error, log: log, "[entitlements] unsupported embedded plist format: %{public}@", header as NSData)
|
||||||
|
return nil // try anyway?
|
||||||
|
}
|
||||||
|
|
||||||
|
// big endian, so no memcpy for us :(
|
||||||
|
let size: UInt32 = (UInt32(data[4]) << 24) | (UInt32(data[5]) << 16) | (UInt32(data[6]) << 8) | UInt32(data[7])
|
||||||
|
if size != data.count {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Plist formatter
|
||||||
|
|
||||||
|
/// Print recursive tree of key-value mappings.
|
||||||
|
func recursiveKeyValue(_ value: Any, _ output: inout String, _ level: Int = -1, _ key: String? = nil) {
|
||||||
|
let indent = level > 0 ? String(repeating: " ", count: level * 4) : ""
|
||||||
|
let prefix = indent + (key?.appending(" = ") ?? "")
|
||||||
|
|
||||||
|
if let dict = value as? [String: Any] {
|
||||||
|
if level > -1 {
|
||||||
|
output.append(prefix + "{\n")
|
||||||
|
}
|
||||||
|
for (subKey, subValue) in dict.sorted(by: { $0.key < $1.key }) {
|
||||||
|
recursiveKeyValue(subValue, &output, level + 1, subKey)
|
||||||
|
}
|
||||||
|
if level > -1 {
|
||||||
|
output.append(indent + "}\n")
|
||||||
|
}
|
||||||
|
} else if let array = value as? [Any] {
|
||||||
|
output.append(prefix + "(\n")
|
||||||
|
for element in array {
|
||||||
|
recursiveKeyValue(element, &output, level + 1, nil)
|
||||||
|
}
|
||||||
|
output.append(indent + ")\n")
|
||||||
|
} else if let data = value as? Data {
|
||||||
|
output.append(prefix + "\(data.count) bytes of data\n")
|
||||||
|
} else {
|
||||||
|
output.append(prefix + "\(value)\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
190
src/Plist.swift
Normal file
190
src/Plist.swift
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import Foundation
|
||||||
|
import os // OSLog
|
||||||
|
|
||||||
|
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Plist")
|
||||||
|
|
||||||
|
|
||||||
|
typealias PlistDict = [String: Any] // basically an untyped Dict
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
|
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: -
|
||||||
|
|
||||||
|
extension QuickLookInfo {
|
||||||
|
/// Read app default `Info.plist`.
|
||||||
|
func readPlistApp() -> PlistDict? {
|
||||||
|
switch self.type {
|
||||||
|
case .IPA, .Archive, .Extension:
|
||||||
|
return self.readPayloadFile("Info.plist")?.asPlistOrNil()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read `iTunesMetadata.plist` if available
|
||||||
|
func readPlistItunes() -> PlistDict? {
|
||||||
|
switch self.type {
|
||||||
|
case .IPA:
|
||||||
|
return self.zipFile!.unzipFile("iTunesMetadata.plist")?.asPlistOrNil()
|
||||||
|
case .Archive, .Extension:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
|
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.
|
||||||
|
func iconNamesFromPlist(_ appPlist: PlistDict?) -> [String] {
|
||||||
|
let appPlist = appPlist == nil ? meta.readPlistApp()! : appPlist!
|
||||||
|
// 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.
|
||||||
|
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) {
|
||||||
|
if zip.sizeUncompressed > 0 {
|
||||||
|
matches.append(zip.filepath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches.count > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
guard let files = try? FileManager.default.contentsOfDirectory(atPath: parentDir) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for file in files {
|
||||||
|
if file.hasPrefix(fileName) {
|
||||||
|
let fullPath = parentDir + "/" + file
|
||||||
|
if let fSize = try? FileManager.default.attributesOfItem(atPath: fullPath)[FileAttributeKey.size] as? Int {
|
||||||
|
if fSize > 0 {
|
||||||
|
matches.append(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches.count > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
let lower = iconName.lowercased()
|
||||||
|
// "defaultX" = launch image
|
||||||
|
let penalty = lower.contains("small") || lower.hasPrefix("default") ? 20 : 0
|
||||||
|
|
||||||
|
let resolutionOrder: [String] = [
|
||||||
|
"@3x", "180", "167", "152", "@2x", "120",
|
||||||
|
"144", "114", "87", "80", "76", "72", "58", "57"
|
||||||
|
]
|
||||||
|
for (i, res) in resolutionOrder.enumerated() {
|
||||||
|
if iconName.contains(res) {
|
||||||
|
return i + penalty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 50 + penalty
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a list of filenames, order them highest resolution first.
|
||||||
|
private func sortedByResolution(_ icons: [String]) -> [String] {
|
||||||
|
return icons.sorted { (icon1, icon2) -> Bool in
|
||||||
|
let index1 = self.resolutionIndex(icon1)
|
||||||
|
let index2 = self.resolutionIndex(icon2)
|
||||||
|
return index1 < index2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
555
src/PreviewGenerator.swift
Normal file
555
src/PreviewGenerator.swift
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
import Foundation
|
||||||
|
import os // OSLog
|
||||||
|
|
||||||
|
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "PreviewGenerator")
|
||||||
|
|
||||||
|
typealias HtmlDict = [String: String] // used for TAG replacements
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Generic data formatting & printing
|
||||||
|
|
||||||
|
typealias TableRow = [String]
|
||||||
|
|
||||||
|
/// Print html table with arbitrary number of columns
|
||||||
|
/// @param header If set, start the table with a `tr` column row.
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print recursive tree of key-value mappings.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace occurrences of chars `&"'<>` with html encoding.
|
||||||
|
func escapeXML(_ stringToEscape: String) -> String {
|
||||||
|
return stringToEscape
|
||||||
|
.replacingOccurrences(of: "&", with: "&")
|
||||||
|
.replacingOccurrences(of: "\"", with: """)
|
||||||
|
.replacingOccurrences(of: "'", with: "'")
|
||||||
|
.replacingOccurrences(of: "<", with: "<")
|
||||||
|
.replacingOccurrences(of: ">", with: ">")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Date processing
|
||||||
|
|
||||||
|
/// @return Difference between two dates as components.
|
||||||
|
func dateDiff(_ start: Date, _ end: Date) -> DateComponents {
|
||||||
|
return Calendar.current.dateComponents([.day, .hour, .minute], from: start, to: end)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @return Print largest component. E.g., "3 days" or "14 hours"
|
||||||
|
func relativeDateString(_ comp: DateComponents) -> String {
|
||||||
|
let formatter = DateComponentsFormatter()
|
||||||
|
formatter.unitsStyle = .full
|
||||||
|
formatter.maximumUnitCount = 1
|
||||||
|
return formatter.string(from: comp)!
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @return Print the date with current locale and medium length style.
|
||||||
|
func formattedDate(_ date: Date) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
formatter.timeStyle = .medium
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse date from plist regardless if it has `NSDate` or `NSString` type.
|
||||||
|
func parseDate(_ value: Any?) -> Date? {
|
||||||
|
if let date = value as? Date {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let stringValue = value as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the date from a string
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
|
||||||
|
if let date = formatter.date(from: stringValue) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
|
||||||
|
if let date = formatter.date(from: stringValue) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
os_log(.error, log: log, "ERROR formatting date: %{public}@", stringValue)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @return Relative distance to today. E.g., "Expired today"
|
||||||
|
func relativeExpirationDateString(_ date: Date) -> String {
|
||||||
|
let isPast = date < Date()
|
||||||
|
let isToday = Calendar.current.isDateInToday(date)
|
||||||
|
|
||||||
|
if isToday {
|
||||||
|
return isPast ? "<span>Expired today</span>" : "<span>Expires today</span>"
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPast {
|
||||||
|
let comp = dateDiff(date, Date())
|
||||||
|
return "<span>Expired \(relativeDateString(comp)) ago</span>"
|
||||||
|
}
|
||||||
|
|
||||||
|
let comp = dateDiff(Date(), date)
|
||||||
|
if comp.day! < 30 {
|
||||||
|
return "<span>Expires in \(relativeDateString(comp))</span>"
|
||||||
|
}
|
||||||
|
return "Expires in \(relativeDateString(comp))"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @return Relative distance to today. E.g., "DATE (Expires in 3 days)"
|
||||||
|
func formattedExpirationDate(_ date: Date) -> String {
|
||||||
|
return "\(formattedDate(date)) (\(relativeExpirationDateString(date)))"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @return Relative distance to today. E.g., "DATE (Created 3 days ago)"
|
||||||
|
func formattedCreationDate(_ date: Date) -> String {
|
||||||
|
let isToday = Calendar.current.isDateInToday(date)
|
||||||
|
let comp = dateDiff(date, Date())
|
||||||
|
return "\(formattedDate(date)) (Created \(isToday ? "today" : "\(relativeDateString(comp)) ago"))"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @return CSS class for expiration status.
|
||||||
|
func classNameForExpirationStatus(_ date: Date?) -> String {
|
||||||
|
switch ExpirationStatus(date) {
|
||||||
|
case .Expired: return "expired"
|
||||||
|
case .Expiring: return "expiring"
|
||||||
|
case .Valid: return "valid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - App Info
|
||||||
|
|
||||||
|
/// @return List of ATS flags.
|
||||||
|
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`
|
||||||
|
func procAppInfo(_ appPlist: PlistDict?) -> HtmlDict {
|
||||||
|
guard let appPlist else {
|
||||||
|
return [
|
||||||
|
"AppInfoHidden": "hiddenDiv",
|
||||||
|
"ProvisionTitleHidden": "",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
return [
|
||||||
|
"AppInfoHidden": "",
|
||||||
|
"ProvisionTitleHidden": "hiddenDiv",
|
||||||
|
|
||||||
|
"CFBundleName": appPlist["CFBundleDisplayName"] as? String ?? appPlist["CFBundleName"] as? String ?? "",
|
||||||
|
"CFBundleShortVersionString": appPlist["CFBundleShortVersionString"] as? String ?? "",
|
||||||
|
"CFBundleVersion": appPlist["CFBundleVersion"] as? String ?? "",
|
||||||
|
"CFBundleIdentifier": appPlist["CFBundleIdentifier"] as? String ?? "",
|
||||||
|
|
||||||
|
"ExtensionTypeHidden": extensionType != nil ? "" : "hiddenDiv",
|
||||||
|
"ExtensionType": extensionType ?? "",
|
||||||
|
|
||||||
|
"UIDeviceFamily": platforms ?? "",
|
||||||
|
"DTSDKName": appPlist["DTSDKName"] as? String ?? "",
|
||||||
|
"MinimumOSVersion": minVersion,
|
||||||
|
"AppTransportSecurityFormatted": formattedAppTransportSecurity(appPlist),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - iTunes Purchase Information
|
||||||
|
|
||||||
|
/// Concatenate all (sub)genres into a comma separated list.
|
||||||
|
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`
|
||||||
|
func parseItunesMeta(_ itunesPlist: PlistDict?) -> HtmlDict {
|
||||||
|
guard let itunesPlist else {
|
||||||
|
return ["iTunesHidden": "hiddenDiv"]
|
||||||
|
}
|
||||||
|
|
||||||
|
let downloadInfo = itunesPlist["com.apple.iTunesStore.downloadInfo"] as? PlistDict
|
||||||
|
let accountInfo = downloadInfo?["accountInfo"] as? PlistDict ?? [:]
|
||||||
|
|
||||||
|
let purchaseDate = parseDate(downloadInfo?["purchaseDate"] ?? itunesPlist["purchaseDate"])
|
||||||
|
let releaseDate = parseDate(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
|
||||||
|
}
|
||||||
|
os_log(.error, log: log, "id: %{public}@", String(describing: itunesPlist["itemId"]))
|
||||||
|
return [
|
||||||
|
"iTunesHidden": "",
|
||||||
|
"iTunesId": (itunesPlist["itemId"] as? Int)?.description ?? "", // description]
|
||||||
|
"iTunesName": itunesPlist["itemName"] as? String ?? "",
|
||||||
|
"iTunesGenres": formattedGenres(itunesPlist),
|
||||||
|
"iTunesReleaseDate": releaseDate == nil ? "" : formattedDate(releaseDate!),
|
||||||
|
|
||||||
|
"iTunesAppleId": name,
|
||||||
|
"iTunesPurchaseDate": purchaseDate == nil ? "" : formattedDate(purchaseDate!),
|
||||||
|
"iTunesPrice": itunesPlist["priceDisplay"] as? String ?? "",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Certificates
|
||||||
|
|
||||||
|
/// Process a single certificate. Extract invalidity / expiration date.
|
||||||
|
/// @param subject just used for printing error logs.
|
||||||
|
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 parseDate(dateString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process list of all certificates. Return a two column table with subject and expiration date.
|
||||||
|
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 = relativeExpirationDateString(invalidityDate)
|
||||||
|
} 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)".
|
||||||
|
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`
|
||||||
|
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`
|
||||||
|
func procProvision(_ provisionPlist: PlistDict?, isOSX: Bool) -> HtmlDict {
|
||||||
|
guard let provisionPlist else {
|
||||||
|
return ["ProvisionHidden": "hiddenDiv"]
|
||||||
|
}
|
||||||
|
|
||||||
|
let creationDate = provisionPlist["CreationDate"] as? Date
|
||||||
|
let expireDate = provisionPlist["ExpirationDate"] as? Date
|
||||||
|
let devices = getDeviceList(provisionPlist)
|
||||||
|
let certs = getCertificateList(provisionPlist)
|
||||||
|
|
||||||
|
return [
|
||||||
|
"ProvisionHidden": "",
|
||||||
|
"ProfileName": provisionPlist["Name"] as? String ?? "",
|
||||||
|
"ProfileUUID": provisionPlist["UUID"] as? String ?? "",
|
||||||
|
"TeamName": provisionPlist["TeamName"] as? String ?? "<em>Team name not available</em>",
|
||||||
|
"TeamIds": (provisionPlist["TeamIdentifier"] as? [String])?.joined(separator: ", ") ?? "<em>Team ID not available</em>",
|
||||||
|
"CreationDateFormatted": creationDate == nil ? "" : formattedCreationDate(creationDate!),
|
||||||
|
"ExpirationDateFormatted": expireDate == nil ? "" : formattedExpirationDate(expireDate!),
|
||||||
|
"ExpStatus": classNameForExpirationStatus(expireDate),
|
||||||
|
|
||||||
|
"ProfilePlatform": isOSX ? "Mac" : "iOS",
|
||||||
|
"ProfileType": stringForProfileType(provisionPlist, isOSX: isOSX),
|
||||||
|
|
||||||
|
"ProvisionedDevicesCount": devices.isEmpty ? "No Devices" : "\(devices.count) Device\(devices.count == 1 ? "" : "s")",
|
||||||
|
"ProvisionedDevicesFormatted": devices.isEmpty ? "Distribution Profile" : formatAsTable(devices, header: ["", "UDID"]),
|
||||||
|
|
||||||
|
"DeveloperCertificatesFormatted": certs.isEmpty ? "No Developer Certificates" : formatAsTable(certs),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Entitlements
|
||||||
|
|
||||||
|
/// Search for app binary and run `codesign` on it.
|
||||||
|
func readEntitlements(_ meta: QuickLookInfo, _ 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`
|
||||||
|
func procEntitlements(_ meta: QuickLookInfo, _ appPlist: PlistDict?, _ provisionPlist: PlistDict?) -> HtmlDict {
|
||||||
|
var entitlements = readEntitlements(meta, appPlist?["CFBundleExecutable"] as? String)
|
||||||
|
entitlements.applyFallbackIfNeeded(provisionPlist?["Entitlements"] as? PlistDict)
|
||||||
|
|
||||||
|
return [
|
||||||
|
"EntitlementsWarningHidden": entitlements.hasError ? "" : "hiddenDiv",
|
||||||
|
"EntitlementsFormatted": entitlements.html ?? "No Entitlements",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - File Info
|
||||||
|
|
||||||
|
/// Title of the preview window
|
||||||
|
func stringForFileType(_ meta: QuickLookInfo) -> String {
|
||||||
|
switch meta.type {
|
||||||
|
case .IPA: return "App info"
|
||||||
|
case .Archive: return "Archive info"
|
||||||
|
case .Extension: return "App extension info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate file / folder size.
|
||||||
|
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.
|
||||||
|
func procFileInfo(_ url: URL) -> HtmlDict {
|
||||||
|
let formattedValue : String
|
||||||
|
if let attrs = try? FileManager.default.attributesOfItem(atPath: url.path) {
|
||||||
|
let size = ByteCountFormatter.string(fromByteCount: getFileSize(url.path), countStyle: .file)
|
||||||
|
formattedValue = "\(size), Modified \(formattedDate(attrs[.modificationDate] as! Date))"
|
||||||
|
} else {
|
||||||
|
formattedValue = ""
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
"FileName": escapeXML(url.lastPathComponent),
|
||||||
|
"FileInfo": formattedValue,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Footer Info
|
||||||
|
|
||||||
|
/// Process meta information about the plugin. Like version and debug flag.
|
||||||
|
func procFooterInfo() -> HtmlDict {
|
||||||
|
#if DEBUG
|
||||||
|
let debugString = "(debug)"
|
||||||
|
#else
|
||||||
|
let debugString = ""
|
||||||
|
#endif
|
||||||
|
return [
|
||||||
|
"DEBUG": debugString,
|
||||||
|
"BundleShortVersionString": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "",
|
||||||
|
"BundleVersion": Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Main Entry
|
||||||
|
|
||||||
|
func applyHtmlTemplate(_ templateValues: HtmlDict) -> String {
|
||||||
|
let templateURL = Bundle.main.url(forResource: "template", withExtension: "html")!
|
||||||
|
let html = try! String(contentsOf: templateURL, encoding: .utf8)
|
||||||
|
|
||||||
|
// this is less efficient
|
||||||
|
// for (key, value) in templateValues {
|
||||||
|
// html = html.replacingOccurrences(of: "__\(key)__", with: value)
|
||||||
|
// }
|
||||||
|
|
||||||
|
var rv = ""
|
||||||
|
var prevLoc = html.startIndex
|
||||||
|
let regex = try! NSRegularExpression(pattern: "__[^ _]{1,40}?__")
|
||||||
|
regex.enumerateMatches(in: html, range: NSRange(location: 0, length: html.count), using: { match, flags, stop in
|
||||||
|
let start = html.index(html.startIndex, offsetBy: match!.range.lowerBound)
|
||||||
|
let key = String(html[html.index(start, offsetBy: 2) ..< html.index(start, offsetBy: match!.range.length - 2)])
|
||||||
|
// append unrelated text up to this key
|
||||||
|
rv.append(contentsOf: html[prevLoc ..< start])
|
||||||
|
prevLoc = html.index(start, offsetBy: match!.range.length)
|
||||||
|
// append key if exists (else remove template-key)
|
||||||
|
if let value = templateValues[key] {
|
||||||
|
rv.append(value)
|
||||||
|
} else {
|
||||||
|
// os_log(.debug, log: log, "unknown template key: %{public}@", key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// append remaining text
|
||||||
|
rv.append(contentsOf: html[prevLoc ..< html.endIndex])
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateHtml(at url: URL) -> String {
|
||||||
|
let meta = QuickLookInfo(url)
|
||||||
|
var infoLayer: HtmlDict = [
|
||||||
|
"AppInfoTitle": stringForFileType(meta),
|
||||||
|
]
|
||||||
|
|
||||||
|
// App Info
|
||||||
|
let plistApp = meta.readPlistApp()
|
||||||
|
infoLayer.merge(procAppInfo(plistApp)) { (_, new) in new }
|
||||||
|
|
||||||
|
let plistItunes = meta.readPlistItunes()
|
||||||
|
infoLayer.merge(parseItunesMeta(plistItunes)) { (_, new) in new }
|
||||||
|
|
||||||
|
// Provisioning
|
||||||
|
let plistProvision = meta.readPlistProvision()
|
||||||
|
infoLayer.merge(procProvision(plistProvision, isOSX: meta.isOSX)) { (_, new) in new }
|
||||||
|
|
||||||
|
// Entitlements
|
||||||
|
let entitlements = procEntitlements(meta, plistApp, plistProvision)
|
||||||
|
infoLayer.merge(entitlements) { (_, new) in new }
|
||||||
|
// File Info
|
||||||
|
infoLayer.merge(procFileInfo(url)) { (_, new) in new }
|
||||||
|
// Footer Info
|
||||||
|
infoLayer.merge(procFooterInfo()) { (_, new) in new }
|
||||||
|
// App Icon (last, because the image uses a lot of memory)
|
||||||
|
let icon = AppIcon(meta)
|
||||||
|
infoLayer["AppIcon"] = icon.extractImage(from: plistApp).withRoundCorners().asBase64()
|
||||||
|
// prepare html, replace values
|
||||||
|
return applyHtmlTemplate(infoLayer)
|
||||||
|
}
|
||||||
58
src/RoundedIcon.swift
Normal file
58
src/RoundedIcon.swift
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import Foundation
|
||||||
|
import AppKit // NSBezierPath
|
||||||
|
|
||||||
|
//
|
||||||
|
// NSBezierPath+IOS7RoundedRect
|
||||||
|
//
|
||||||
|
// Created by Matej Dunik on 11/12/13.
|
||||||
|
// Copyright (c) 2013 PixelCut. All rights reserved except as below:
|
||||||
|
// This code is provided as-is, without warranty of any kind. You may use it in your projects as you wish.
|
||||||
|
//
|
||||||
|
|
||||||
|
extension NSBezierPath {
|
||||||
|
public class func IOS7RoundedRect(_ rect: NSRect, cornerRadius: CGFloat) -> NSBezierPath {
|
||||||
|
let path = NSBezierPath()
|
||||||
|
let limit = min(rect.size.width, rect.size.height) / 2 / 1.52866483
|
||||||
|
let limitedRadius = min(cornerRadius, limit)
|
||||||
|
|
||||||
|
@inline(__always) func topLeft(_ x: CGFloat, _ y: CGFloat) -> NSPoint {
|
||||||
|
return NSPoint(x: rect.origin.x + x * limitedRadius, y: rect.origin.y + y * limitedRadius)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inline(__always) func topRight(_ x: CGFloat, _ y: CGFloat) -> NSPoint {
|
||||||
|
return NSPoint(x: rect.origin.x + rect.size.width - x * limitedRadius, y: rect.origin.y + y * limitedRadius)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inline(__always) func bottomRight(_ x: CGFloat, _ y: CGFloat) -> NSPoint {
|
||||||
|
return NSPoint(x: rect.origin.x + rect.size.width - x * limitedRadius, y: rect.origin.y + rect.size.height - y * limitedRadius)
|
||||||
|
}
|
||||||
|
|
||||||
|
@inline(__always) func bottomLeft(_ x: CGFloat, _ y: CGFloat) -> NSPoint {
|
||||||
|
return NSPoint(x: rect.origin.x + x * limitedRadius, y: rect.origin.y + rect.size.height - y * limitedRadius)
|
||||||
|
}
|
||||||
|
|
||||||
|
path.move(to: topLeft(1.52866483, 0.00000000))
|
||||||
|
path.line(to: topRight(1.52866471, 0.00000000))
|
||||||
|
path.curve(to: topRight(0.66993427, 0.06549600), controlPoint1: topRight(1.08849323, 0.00000000), controlPoint2: topRight(0.86840689, 0.00000000))
|
||||||
|
path.line(to: topRight(0.63149399, 0.07491100))
|
||||||
|
path.curve(to: topRight(0.07491176, 0.63149399), controlPoint1: topRight(0.37282392, 0.16905899), controlPoint2: topRight(0.16906013, 0.37282401))
|
||||||
|
path.curve(to: topRight(0.00000000, 1.52866483), controlPoint1: topRight(0.00000000, 0.86840701), controlPoint2: topRight(0.00000000, 1.08849299))
|
||||||
|
path.line(to: bottomRight(0.00000000, 1.52866471))
|
||||||
|
path.curve(to: bottomRight(0.06549569, 0.66993493), controlPoint1: bottomRight(0.00000000, 1.08849323), controlPoint2: bottomRight(0.00000000, 0.86840689))
|
||||||
|
path.line(to: bottomRight(0.07491111, 0.63149399))
|
||||||
|
path.curve(to: bottomRight(0.63149399, 0.07491111), controlPoint1: bottomRight(0.16905883, 0.37282392), controlPoint2: bottomRight(0.37282392, 0.16905883))
|
||||||
|
path.curve(to: bottomRight(1.52866471, 0.00000000), controlPoint1: bottomRight(0.86840689, 0.00000000), controlPoint2: bottomRight(1.08849323, 0.00000000))
|
||||||
|
path.line(to: bottomLeft(1.52866483, 0.00000000))
|
||||||
|
path.curve(to: bottomLeft(0.66993397, 0.06549569), controlPoint1: bottomLeft(1.08849299, 0.00000000), controlPoint2: bottomLeft(0.86840701, 0.00000000))
|
||||||
|
path.line(to: bottomLeft(0.63149399, 0.07491111))
|
||||||
|
path.curve(to: bottomLeft(0.07491100, 0.63149399), controlPoint1: bottomLeft(0.37282401, 0.16905883), controlPoint2: bottomLeft(0.16906001, 0.37282392))
|
||||||
|
path.curve(to: bottomLeft(0.00000000, 1.52866471), controlPoint1: bottomLeft(0.00000000, 0.86840689), controlPoint2: bottomLeft(0.00000000, 1.08849323))
|
||||||
|
path.line(to: topLeft(0.00000000, 1.52866483))
|
||||||
|
path.curve(to: topLeft(0.06549600, 0.66993397), controlPoint1: topLeft(0.00000000, 1.08849299), controlPoint2: topLeft(0.00000000, 0.86840701))
|
||||||
|
path.line(to: topLeft(0.07491100, 0.63149399))
|
||||||
|
path.curve(to: topLeft(0.63149399, 0.07491100), controlPoint1: topLeft(0.16906001, 0.37282401), controlPoint2: topLeft(0.37282401, 0.16906001))
|
||||||
|
path.curve(to: topLeft(1.52866483, 0.00000000), controlPoint1: topLeft(0.86840701, 0.00000000), controlPoint2: topLeft(1.08849299, 0.00000000))
|
||||||
|
path.close()
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/Shared.swift
Normal file
91
src/Shared.swift
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import Foundation
|
||||||
|
import os // OSLog
|
||||||
|
|
||||||
|
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Shared")
|
||||||
|
|
||||||
|
|
||||||
|
// Init QuickLook Type
|
||||||
|
enum FileType {
|
||||||
|
case IPA
|
||||||
|
case Archive
|
||||||
|
case Extension
|
||||||
|
}
|
||||||
|
|
||||||
|
struct QuickLookInfo {
|
||||||
|
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
|
||||||
|
|
||||||
|
/// 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":
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Meta data for QuickLook
|
||||||
|
|
||||||
|
/// Search an archive for the .app or .ipa bundle.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Other helper
|
||||||
|
|
||||||
|
enum ExpirationStatus {
|
||||||
|
case Expired
|
||||||
|
case Expiring
|
||||||
|
case Valid
|
||||||
|
|
||||||
|
/// Check time between date and now. Set Expiring if less than 30 days until expiration
|
||||||
|
init(_ date: Date?) {
|
||||||
|
if date == nil || date!.timeIntervalSinceNow < 0 {
|
||||||
|
self = .Expired
|
||||||
|
}
|
||||||
|
let components = Calendar.current.dateComponents([.day], from: Date(), to: date!)
|
||||||
|
self = components.day! < 30 ? .Expiring : .Valid
|
||||||
|
}
|
||||||
|
}
|
||||||
162
src/ThumbnailGenerator.swift
Normal file
162
src/ThumbnailGenerator.swift
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
////#import "Shared.h"
|
||||||
|
//#import "AppIcon.h"
|
||||||
|
//
|
||||||
|
//// makro to stop further processing
|
||||||
|
//#define ALLOW_EXIT if (QLThumbnailRequestIsCancelled(thumbnail)) { return noErr; }
|
||||||
|
//
|
||||||
|
////Layout constants
|
||||||
|
//#define BADGE_MARGIN 10.0
|
||||||
|
//#define MIN_BADGE_WIDTH 40.0
|
||||||
|
//#define BADGE_HEIGHT 75.0
|
||||||
|
//#define BADGE_MARGIN_X 60.0
|
||||||
|
//#define BADGE_MARGIN_Y 80.0
|
||||||
|
//
|
||||||
|
////Drawing constants
|
||||||
|
//#define BADGE_BG_COLOR [NSColor lightGrayColor]
|
||||||
|
//#define BADGE_VALID_COLOR [NSColor colorWithCalibratedRed:(0/255.0) green:(98/255.0) blue:(25/255.0) alpha:1]
|
||||||
|
//#define BADGE_EXPIRING_COLOR [NSColor colorWithCalibratedRed:(146/255.0) green:(95/255.0) blue:(28/255.0) alpha:1]
|
||||||
|
//#define BADGE_EXPIRED_COLOR [NSColor colorWithCalibratedRed:(141/255.0) green:(0/255.0) blue:(7/255.0) alpha:1]
|
||||||
|
//#define BADGE_FONT [NSFont boldSystemFontOfSize:64]
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thumbnail, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options, CGSize maxSize);
|
||||||
|
//void CancelThumbnailGeneration(void *thisInterface, QLThumbnailRequestRef thumbnail);
|
||||||
|
//
|
||||||
|
///* -----------------------------------------------------------------------------
|
||||||
|
// Generate a thumbnail for file
|
||||||
|
//
|
||||||
|
// This function's job is to create thumbnail for designated file as fast as possible
|
||||||
|
// ----------------------------------------------------------------------------- */
|
||||||
|
//
|
||||||
|
//// MARK: .ipa .xcarchive
|
||||||
|
//
|
||||||
|
//OSStatus renderAppIcon(QuickLookInfo meta, QLThumbnailRequestRef thumbnail) {
|
||||||
|
// AppIcon *icon = [AppIcon load:meta];
|
||||||
|
// if (!icon.canExtractImage) {
|
||||||
|
// return noErr;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // set magic flag to draw icon without additional markers
|
||||||
|
// static const NSString *IconFlavor;
|
||||||
|
// if (@available(macOS 10.15, *)) {
|
||||||
|
// IconFlavor = @"icon";
|
||||||
|
// } else {
|
||||||
|
// IconFlavor = @"IconFlavor";
|
||||||
|
// }
|
||||||
|
// NSDictionary *propertiesDict = nil;
|
||||||
|
// if (meta.type == FileTypeArchive) {
|
||||||
|
// // 0: Plain transparent, 1: Shadow, 2: Book, 3: Movie, 4: Address, 5: Image,
|
||||||
|
// // 6: Gloss, 7: Slide, 8: Square, 9: Border, 11: Calendar, 12: Pattern
|
||||||
|
// propertiesDict = @{IconFlavor : @(12)}; // looks like "in development"
|
||||||
|
// } else {
|
||||||
|
// propertiesDict = @{IconFlavor : @(0)}; // no border, no anything
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// NSImage *appIcon = [[icon extractImage:nil] withRoundCorners];
|
||||||
|
// ALLOW_EXIT
|
||||||
|
//
|
||||||
|
// // image-only icons can be drawn efficiently by calling `SetImage` directly.
|
||||||
|
// QLThumbnailRequestSetImageWithData(thumbnail, (__bridge CFDataRef)[appIcon TIFFRepresentation], (__bridge CFDictionaryRef)propertiesDict);
|
||||||
|
// return noErr;
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//// MARK: .provisioning
|
||||||
|
//
|
||||||
|
//OSStatus renderProvision(QuickLookInfo meta, QLThumbnailRequestRef thumbnail, BOOL iconMode) {
|
||||||
|
// NSDictionary *propertyList = readPlistProvision(meta);
|
||||||
|
// ALLOW_EXIT
|
||||||
|
//
|
||||||
|
// NSUInteger devicesCount = arrayOrNil(propertyList[@"ProvisionedDevices"]).count;
|
||||||
|
// NSDate *expirationDate = dateOrNil(propertyList[@"ExpirationDate"]);
|
||||||
|
//
|
||||||
|
// NSImage *appIcon = nil;
|
||||||
|
// if (iconMode) {
|
||||||
|
// NSURL *iconURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"blankIcon" withExtension:@"png"];
|
||||||
|
// appIcon = [[NSImage alloc] initWithContentsOfURL:iconURL];
|
||||||
|
// } else {
|
||||||
|
// appIcon = [[NSWorkspace sharedWorkspace] iconForFileType:meta.UTI];
|
||||||
|
// [appIcon setSize:NSMakeSize(512, 512)];
|
||||||
|
// }
|
||||||
|
// ALLOW_EXIT
|
||||||
|
//
|
||||||
|
// NSRect renderRect = NSMakeRect(0.0, 0.0, appIcon.size.width, appIcon.size.height);
|
||||||
|
//
|
||||||
|
// // Font attributes
|
||||||
|
// NSColor *outlineColor;
|
||||||
|
// switch (expirationStatus(expirationDate)) {
|
||||||
|
// case ExpirationStatusExpired: outlineColor = BADGE_EXPIRED_COLOR; break;
|
||||||
|
// case ExpirationStatusExpiring: outlineColor = BADGE_EXPIRING_COLOR; break;
|
||||||
|
// case ExpirationStatusValid: outlineColor = BADGE_VALID_COLOR; break;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
|
||||||
|
// paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||||
|
// paragraphStyle.alignment = NSTextAlignmentCenter;
|
||||||
|
//
|
||||||
|
// NSDictionary *fontAttrs = @{
|
||||||
|
// NSFontAttributeName : BADGE_FONT,
|
||||||
|
// NSForegroundColorAttributeName : outlineColor,
|
||||||
|
// NSParagraphStyleAttributeName: paragraphStyle
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// // Badge size & placement
|
||||||
|
// int badgeX = renderRect.origin.x + BADGE_MARGIN_X;
|
||||||
|
// int badgeY = renderRect.origin.y + renderRect.size.height - BADGE_HEIGHT - BADGE_MARGIN_Y;
|
||||||
|
// if (!iconMode) {
|
||||||
|
// badgeX += 75;
|
||||||
|
// badgeY -= 10;
|
||||||
|
// }
|
||||||
|
// int badgeNumX = badgeX + BADGE_MARGIN;
|
||||||
|
// NSPoint badgeTextPoint = NSMakePoint(badgeNumX, badgeY);
|
||||||
|
//
|
||||||
|
// NSString *badge = [NSString stringWithFormat:@"%lu",(unsigned long)devicesCount];
|
||||||
|
// NSSize badgeNumSize = [badge sizeWithAttributes:fontAttrs];
|
||||||
|
// int badgeWidth = badgeNumSize.width + BADGE_MARGIN * 2;
|
||||||
|
// NSRect badgeOutlineRect = NSMakeRect(badgeX, badgeY, MAX(badgeWidth, MIN_BADGE_WIDTH), BADGE_HEIGHT);
|
||||||
|
//
|
||||||
|
// // Do as much work as possible before the `CreateContext`. We can try to quit early before that!
|
||||||
|
// CGContextRef _context = QLThumbnailRequestCreateContext(thumbnail, renderRect.size, false, NULL);
|
||||||
|
// if (_context) {
|
||||||
|
// NSGraphicsContext *_graphicsContext = [NSGraphicsContext graphicsContextWithCGContext:(void *)_context flipped:NO];
|
||||||
|
// [NSGraphicsContext setCurrentContext:_graphicsContext];
|
||||||
|
// [appIcon drawInRect:renderRect];
|
||||||
|
//
|
||||||
|
// NSBezierPath *badgePath = [NSBezierPath bezierPathWithRoundedRect:badgeOutlineRect xRadius:10 yRadius:10];
|
||||||
|
// [badgePath setLineWidth:8.0];
|
||||||
|
// [BADGE_BG_COLOR set];
|
||||||
|
// [badgePath fill];
|
||||||
|
// [outlineColor set];
|
||||||
|
// [badgePath stroke];
|
||||||
|
//
|
||||||
|
// [badge drawAtPoint:badgeTextPoint withAttributes:fontAttrs];
|
||||||
|
//
|
||||||
|
// QLThumbnailRequestFlushContext(thumbnail, _context);
|
||||||
|
// CFRelease(_context);
|
||||||
|
// }
|
||||||
|
// return noErr;
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//// MARK: Main Entry
|
||||||
|
//
|
||||||
|
//OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thumbnail, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options, CGSize maxSize) {
|
||||||
|
// @autoreleasepool {
|
||||||
|
// QuickLookInfo meta = initQLInfo(contentTypeUTI, url);
|
||||||
|
//
|
||||||
|
// if (meta.type == FileTypeProvision) {
|
||||||
|
// NSDictionary *optionsDict = (__bridge NSDictionary *)options;
|
||||||
|
// BOOL iconMode = ([optionsDict objectForKey:(NSString *)kQLThumbnailOptionIconModeKey]) ? YES : NO;
|
||||||
|
// return renderProvision(meta, thumbnail, iconMode);
|
||||||
|
// } else {
|
||||||
|
// return renderAppIcon(meta, thumbnail);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return noErr;
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//void CancelThumbnailGeneration(void *thisInterface, QLThumbnailRequestRef thumbnail) {
|
||||||
|
// // Implement only if supported
|
||||||
|
//}
|
||||||
366
src/Zip.swift
Normal file
366
src/Zip.swift
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
import Foundation
|
||||||
|
import Compression // compression_decode_buffer
|
||||||
|
import zlib // Z_DEFLATED, crc32
|
||||||
|
import os // OSLog
|
||||||
|
|
||||||
|
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Zip")
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Helper to parse byte headers
|
||||||
|
|
||||||
|
private struct ByteScanner {
|
||||||
|
private let data: Data
|
||||||
|
private var index: Int
|
||||||
|
private let endIndex: Int
|
||||||
|
|
||||||
|
init (_ data: Data, start: Int) {
|
||||||
|
self.data = data
|
||||||
|
self.index = start
|
||||||
|
self.endIndex = data.endIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func scan<T>() -> T {
|
||||||
|
let newIndex = index + MemoryLayout<T>.size
|
||||||
|
if newIndex > endIndex {
|
||||||
|
os_log(.fault, log: log, "ByteScanner out of bounds")
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
let result = data.subdata(in: index ..< newIndex).withUnsafeBytes { $0.load(as: T.self) }
|
||||||
|
index = newIndex
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func scanString(length: Int) -> String {
|
||||||
|
let bytes = data.subdata(in: index ..< index + length)
|
||||||
|
index += length
|
||||||
|
return String(data: bytes, encoding: .utf8) ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - ZIP Headers
|
||||||
|
|
||||||
|
// See http://en.wikipedia.org/wiki/ZIP_(file_format)#File_headers
|
||||||
|
|
||||||
|
/// Local file header
|
||||||
|
private struct ZIP_LocalFile {
|
||||||
|
static let LENGTH: Int = 30
|
||||||
|
|
||||||
|
let magicNumber: UInt32 // 50 4B 03 04
|
||||||
|
let versionNeededToExtract: UInt16
|
||||||
|
let generalPurposeBitFlag: UInt16
|
||||||
|
let compressionMethod: UInt16
|
||||||
|
let fileLastModificationTime: UInt16
|
||||||
|
let fileLastModificationDate: UInt16
|
||||||
|
let CRC32: UInt32
|
||||||
|
let compressedSize: UInt32
|
||||||
|
let uncompressedSize: UInt32
|
||||||
|
let fileNameLength: UInt16
|
||||||
|
let extraFieldLength: UInt16
|
||||||
|
|
||||||
|
// let fileName: String
|
||||||
|
// Extra field
|
||||||
|
|
||||||
|
init(_ data: Data, start: Data.Index = 0) {
|
||||||
|
var scanner = ByteScanner(data, start: start)
|
||||||
|
magicNumber = scanner.scan()
|
||||||
|
versionNeededToExtract = scanner.scan()
|
||||||
|
generalPurposeBitFlag = scanner.scan()
|
||||||
|
compressionMethod = scanner.scan()
|
||||||
|
fileLastModificationTime = scanner.scan()
|
||||||
|
fileLastModificationDate = scanner.scan()
|
||||||
|
CRC32 = scanner.scan()
|
||||||
|
compressedSize = scanner.scan()
|
||||||
|
uncompressedSize = scanner.scan()
|
||||||
|
fileNameLength = scanner.scan()
|
||||||
|
extraFieldLength = scanner.scan()
|
||||||
|
// fileName = scanner.scanString(length: Int(fileNameLength))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Central directory file header
|
||||||
|
private struct ZIP_CDFH {
|
||||||
|
static let LENGTH: Int = 46
|
||||||
|
|
||||||
|
let magicNumber: UInt32 // 50 4B 01 02
|
||||||
|
let versionMadeBy: UInt16
|
||||||
|
let versionNeededToExtract: UInt16
|
||||||
|
let generalPurposeBitFlag: UInt16
|
||||||
|
let compressionMethod: UInt16
|
||||||
|
let fileLastModificationTime: UInt16
|
||||||
|
let fileLastModificationDate: UInt16
|
||||||
|
let CRC32: UInt32
|
||||||
|
let compressedSize: UInt32
|
||||||
|
let uncompressedSize: UInt32
|
||||||
|
let fileNameLength: UInt16
|
||||||
|
let extraFieldLength: UInt16
|
||||||
|
let fileCommentLength: UInt16
|
||||||
|
let diskNumberWhereFileStarts: UInt16
|
||||||
|
let internalFileAttributes: UInt16
|
||||||
|
let externalFileAttributes: UInt32
|
||||||
|
let relativeOffsetOfLocalFileHeader: UInt32
|
||||||
|
|
||||||
|
let fileName: String
|
||||||
|
// Extra field
|
||||||
|
// File comment
|
||||||
|
|
||||||
|
init(_ data: Data, start: Data.Index = 0) {
|
||||||
|
var scanner = ByteScanner(data, start: start)
|
||||||
|
magicNumber = scanner.scan()
|
||||||
|
versionMadeBy = scanner.scan()
|
||||||
|
versionNeededToExtract = scanner.scan()
|
||||||
|
generalPurposeBitFlag = scanner.scan()
|
||||||
|
compressionMethod = scanner.scan()
|
||||||
|
fileLastModificationTime = scanner.scan()
|
||||||
|
fileLastModificationDate = scanner.scan()
|
||||||
|
CRC32 = scanner.scan()
|
||||||
|
compressedSize = scanner.scan()
|
||||||
|
uncompressedSize = scanner.scan()
|
||||||
|
fileNameLength = scanner.scan()
|
||||||
|
extraFieldLength = scanner.scan()
|
||||||
|
fileCommentLength = scanner.scan()
|
||||||
|
diskNumberWhereFileStarts = scanner.scan()
|
||||||
|
internalFileAttributes = scanner.scan()
|
||||||
|
externalFileAttributes = scanner.scan()
|
||||||
|
relativeOffsetOfLocalFileHeader = scanner.scan()
|
||||||
|
fileName = scanner.scanString(length: Int(fileNameLength))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End of central directory record
|
||||||
|
private struct ZIP_EOCD {
|
||||||
|
static let LENGTH: Int = 22
|
||||||
|
|
||||||
|
let magicNumber: UInt32 // 50 4B 05 06
|
||||||
|
let numberOfThisDisk: UInt16
|
||||||
|
let diskWhereCentralDirectoryStarts: UInt16
|
||||||
|
let numberOfCentralDirectoryRecordsOnThisDisk: UInt16
|
||||||
|
let totalNumberOfCentralDirectoryRecords: UInt16
|
||||||
|
let sizeOfCentralDirectory: UInt32
|
||||||
|
let offsetOfStartOfCentralDirectory: UInt32
|
||||||
|
let commentLength: UInt16
|
||||||
|
// Comment
|
||||||
|
|
||||||
|
init(_ data: Data, start: Data.Index = 0) {
|
||||||
|
var scanner = ByteScanner(data, start: start)
|
||||||
|
magicNumber = scanner.scan()
|
||||||
|
numberOfThisDisk = scanner.scan()
|
||||||
|
diskWhereCentralDirectoryStarts = scanner.scan()
|
||||||
|
numberOfCentralDirectoryRecordsOnThisDisk = scanner.scan()
|
||||||
|
totalNumberOfCentralDirectoryRecords = scanner.scan()
|
||||||
|
sizeOfCentralDirectory = scanner.scan()
|
||||||
|
offsetOfStartOfCentralDirectory = scanner.scan()
|
||||||
|
commentLength = scanner.scan()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - CRC32 check
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
func crc() -> UInt32 {
|
||||||
|
return UInt32(self.withUnsafeBytes { crc32(0, $0.baseAddress!, UInt32($0.count)) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Unzip data
|
||||||
|
|
||||||
|
func unzipFileEntry(_ path: String, _ entry: ZipEntry) -> Data? {
|
||||||
|
guard let fp = FileHandle(forReadingAtPath: path) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer {
|
||||||
|
try? fp.close()
|
||||||
|
}
|
||||||
|
fp.seek(toFileOffset: UInt64(entry.offset))
|
||||||
|
let file_record = ZIP_LocalFile(fp.readData(ofLength: ZIP_LocalFile.LENGTH))
|
||||||
|
os_log(.debug, log: log, "header: %{public}@ vs %{public}@", String(describing: file_record), String(describing: entry))
|
||||||
|
|
||||||
|
// central directory size and local file size may differ! use local file for ground truth
|
||||||
|
let dataOffset = Int(entry.offset) + ZIP_LocalFile.LENGTH + Int(file_record.fileNameLength) + Int(file_record.extraFieldLength)
|
||||||
|
fp.seek(toFileOffset: UInt64(dataOffset))
|
||||||
|
let rawData = fp.readData(ofLength: Int(entry.sizeCompressed))
|
||||||
|
|
||||||
|
if entry.method == Z_DEFLATED {
|
||||||
|
let size = Int(entry.sizeUncompressed)
|
||||||
|
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: size)
|
||||||
|
defer {
|
||||||
|
buffer.deallocate()
|
||||||
|
}
|
||||||
|
|
||||||
|
let uncompressedData = rawData.withUnsafeBytes ({
|
||||||
|
let ptr = $0.baseAddress!.bindMemory(to: UInt8.self, capacity: 1)
|
||||||
|
let read = compression_decode_buffer(buffer, size, ptr, Int(entry.sizeCompressed), nil, COMPRESSION_ZLIB)
|
||||||
|
return Data(bytes: buffer, count:read)
|
||||||
|
})
|
||||||
|
if file_record.CRC32 != 0, uncompressedData.crc() != file_record.CRC32 {
|
||||||
|
os_log(.error, log: log, "CRC check failed (after uncompress)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return uncompressedData
|
||||||
|
|
||||||
|
} else if entry.method == 0 {
|
||||||
|
if file_record.CRC32 != 0, rawData.crc() != file_record.CRC32 {
|
||||||
|
os_log(.error, log: log, "CRC check failed (uncompressed data)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return rawData
|
||||||
|
|
||||||
|
} else {
|
||||||
|
os_log(.error, log: log, "unimplemented compression method: %{public}d", entry.method)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - List files
|
||||||
|
|
||||||
|
private func listZip(_ path: String) -> [ZipEntry] {
|
||||||
|
guard let fp = FileHandle(forReadingAtPath: path) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
defer {
|
||||||
|
try? fp.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let endRecord = findCentralDirectory(fp), endRecord.sizeOfCentralDirectory > 0 else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return listDirectoryEntries(fp, endRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find signature for central directory.
|
||||||
|
private func findCentralDirectory(_ fp: FileHandle) -> ZIP_EOCD? {
|
||||||
|
let eof = fp.seekToEndOfFile()
|
||||||
|
fp.seek(toFileOffset: max(0, eof - 4096))
|
||||||
|
let data = fp.readDataToEndOfFile()
|
||||||
|
|
||||||
|
let centralDirSignature: [UInt8] = [0x50, 0x4b, 0x05, 0x06]
|
||||||
|
|
||||||
|
guard let range = data.lastRange(of: centralDirSignature) else {
|
||||||
|
os_log(.error, log: log, "no zip end-header found!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ZIP_EOCD(data, start: range.lowerBound)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all files and folders of of the central directory.
|
||||||
|
private func listDirectoryEntries(_ fp: FileHandle, _ centralDir: ZIP_EOCD) -> [ZipEntry] {
|
||||||
|
fp.seek(toFileOffset: UInt64(centralDir.offsetOfStartOfCentralDirectory))
|
||||||
|
let data = fp.readData(ofLength: Int(centralDir.sizeOfCentralDirectory))
|
||||||
|
let total = data.count
|
||||||
|
|
||||||
|
var idx = 0
|
||||||
|
var entries: [ZipEntry] = []
|
||||||
|
|
||||||
|
while idx + ZIP_CDFH.LENGTH < total {
|
||||||
|
let record = ZIP_CDFH(data, start: idx)
|
||||||
|
// read filename
|
||||||
|
idx += ZIP_CDFH.LENGTH
|
||||||
|
let filename = String(data: data.subdata(in: idx ..< idx + Int(record.fileNameLength)), encoding: .utf8)!
|
||||||
|
entries.append(ZipEntry(filename, record))
|
||||||
|
// update index
|
||||||
|
idx += Int(record.fileNameLength + record.extraFieldLength + record.fileCommentLength)
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - ZipEntry
|
||||||
|
|
||||||
|
struct ZipEntry {
|
||||||
|
let filepath: String
|
||||||
|
let offset: UInt32
|
||||||
|
let method: UInt16
|
||||||
|
let sizeCompressed: UInt32
|
||||||
|
let sizeUncompressed: UInt32
|
||||||
|
let filenameLength: UInt16
|
||||||
|
let extraFieldLength: UInt16
|
||||||
|
let CRC32: UInt32
|
||||||
|
|
||||||
|
fileprivate init(_ filename: String, _ record: ZIP_CDFH) {
|
||||||
|
self.filepath = filename
|
||||||
|
self.offset = record.relativeOffsetOfLocalFileHeader
|
||||||
|
self.method = record.compressionMethod
|
||||||
|
self.sizeCompressed = record.compressedSize
|
||||||
|
self.sizeUncompressed = record.uncompressedSize
|
||||||
|
self.filenameLength = record.fileNameLength
|
||||||
|
self.extraFieldLength = record.extraFieldLength
|
||||||
|
self.CRC32 = record.CRC32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Array where Element == ZipEntry {
|
||||||
|
/// Return entry with shortest possible path (thus ignoring deeper nested files).
|
||||||
|
func zipEntryWithShortestPath() -> ZipEntry? {
|
||||||
|
var shortest = 99999
|
||||||
|
var bestMatch: ZipEntry? = nil
|
||||||
|
|
||||||
|
for entry in self {
|
||||||
|
if shortest > entry.filepath.count {
|
||||||
|
shortest = entry.filepath.count
|
||||||
|
bestMatch = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - ZipFile
|
||||||
|
|
||||||
|
struct ZipFile {
|
||||||
|
private let pathToZipFile: String
|
||||||
|
private let centralDirectory: [ZipEntry]
|
||||||
|
|
||||||
|
init(_ path: String) {
|
||||||
|
self.pathToZipFile = path
|
||||||
|
self.centralDirectory = listZip(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - public methods
|
||||||
|
|
||||||
|
func filesMatching(_ path: String) -> [ZipEntry] {
|
||||||
|
let parts = path.split(separator: "*", omittingEmptySubsequences: false)
|
||||||
|
return centralDirectory.filter {
|
||||||
|
var idx = $0.filepath.startIndex
|
||||||
|
if !$0.filepath.hasPrefix(parts.first!) || !$0.filepath.hasSuffix(parts.last!) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for part in parts {
|
||||||
|
guard let found = $0.filepath.range(of: part, range: idx..<$0.filepath.endIndex) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
idx = found.upperBound
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unzip file directly into memory.
|
||||||
|
/// @param filePath File path inside zip file.
|
||||||
|
func unzipFile(_ filePath: String) -> Data? {
|
||||||
|
if let matchingFile = self.filesMatching(filePath).zipEntryWithShortestPath() {
|
||||||
|
os_log(.debug, log: log, "[unzip] %{public}@", matchingFile.filepath)
|
||||||
|
return unzipFileEntry(pathToZipFile, matchingFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a dir listing but no matching file.
|
||||||
|
// This means there wont be anything to extract.
|
||||||
|
os_log(.error, log: log, "cannot find '%{public}@' for unzip", filePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user