diff --git a/CHANGELOG.md b/CHANGELOG.md index f4f2704..e6e2a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # ProvisionQL +## Version 1.7.0 + +* New: show iTunes Metadata & purchase information +* New: use higher resolution app icon if available (try `iTunesArtwork`) +* New: show entitlements regardless of provisioning plist if available +* New: load icon from `Assets.car` +* Performance: unzip with zlib instead of sys-call +* Performance: parse html template tags with regex +* Performance: use `SecCodeSigning` instead of `codesign` sys-call +* Fix codesign unkown param on <10.15 (`--xml` flag) +* Fix crash if a plist key is not present (e.g. `CFBundleShortVersionString` for some old iOS 3.2 ipa) +* Fix fixed-width size for preview of app-icon (consistency) +* Fix `IconFlavor` attribute for thumbnail drawing in 10.15+ +* Fix prefer icons without "small" siffix +* Minor html template improvements +* Some refactoring to reduce duplicate code + ## Version 1.6.4 * Adds error handling to entitlements parsing ([#47](https://github.com/ealeksandrov/ProvisionQL/pull/47)) diff --git a/NSBezierPath+IOS7RoundedRect/NSBezierPath+IOS7RoundedRect.h b/NSBezierPath+IOS7RoundedRect/NSBezierPath+IOS7RoundedRect.h deleted file mode 100644 index 74f4088..0000000 --- a/NSBezierPath+IOS7RoundedRect/NSBezierPath+IOS7RoundedRect.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// NSBezierPath+IOS7RoundedRect.h -// -// 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. -// - -#import - -@interface NSBezierPath (IOS7RoundedRect) - -+ (NSBezierPath *)bezierPathWithIOS7RoundedRect:(NSRect)rect cornerRadius:(CGFloat)radius; - -@end diff --git a/NSBezierPath+IOS7RoundedRect/NSBezierPath+IOS7RoundedRect.m b/NSBezierPath+IOS7RoundedRect/NSBezierPath+IOS7RoundedRect.m deleted file mode 100644 index df7b911..0000000 --- a/NSBezierPath+IOS7RoundedRect/NSBezierPath+IOS7RoundedRect.m +++ /dev/null @@ -1,49 +0,0 @@ -// -// NSBezierPath+IOS7RoundedRect.m -// -// 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. -// - -#import "NSBezierPath+IOS7RoundedRect.h" - -@implementation NSBezierPath (IOS7RoundedRect) - -#define TOP_LEFT(X, Y) NSMakePoint(rect.origin.x + X * limitedRadius, rect.origin.y + Y * limitedRadius) -#define TOP_RIGHT(X, Y) NSMakePoint(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + Y * limitedRadius) -#define BOTTOM_RIGHT(X, Y) NSMakePoint(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) -#define BOTTOM_LEFT(X, Y) NSMakePoint(rect.origin.x + X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) - - -+ (NSBezierPath *)bezierPathWithIOS7RoundedRect:(NSRect)rect cornerRadius:(CGFloat)radius { - NSBezierPath *path = NSBezierPath.bezierPath; - CGFloat limit = MIN(rect.size.width, rect.size.height) / 2 / 1.52866483; - CGFloat limitedRadius = MIN(radius, limit); - - [path moveToPoint: TOP_LEFT(1.52866483, 0.00000000)]; - [path lineToPoint: TOP_RIGHT(1.52866471, 0.00000000)]; - [path curveToPoint: TOP_RIGHT(0.66993427, 0.06549600) controlPoint1: TOP_RIGHT(1.08849323, 0.00000000) controlPoint2: TOP_RIGHT(0.86840689, 0.00000000)]; - [path lineToPoint: TOP_RIGHT(0.63149399, 0.07491100)]; - [path curveToPoint: TOP_RIGHT(0.07491176, 0.63149399) controlPoint1: TOP_RIGHT(0.37282392, 0.16905899) controlPoint2: TOP_RIGHT(0.16906013, 0.37282401)]; - [path curveToPoint: TOP_RIGHT(0.00000000, 1.52866483) controlPoint1: TOP_RIGHT(0.00000000, 0.86840701) controlPoint2: TOP_RIGHT(0.00000000, 1.08849299)]; - [path lineToPoint: BOTTOM_RIGHT(0.00000000, 1.52866471)]; - [path curveToPoint: BOTTOM_RIGHT(0.06549569, 0.66993493) controlPoint1: BOTTOM_RIGHT(0.00000000, 1.08849323) controlPoint2: BOTTOM_RIGHT(0.00000000, 0.86840689)]; - [path lineToPoint: BOTTOM_RIGHT(0.07491111, 0.63149399)]; - [path curveToPoint: BOTTOM_RIGHT(0.63149399, 0.07491111) controlPoint1: BOTTOM_RIGHT(0.16905883, 0.37282392) controlPoint2: BOTTOM_RIGHT(0.37282392, 0.16905883)]; - [path curveToPoint: BOTTOM_RIGHT(1.52866471, 0.00000000) controlPoint1: BOTTOM_RIGHT(0.86840689, 0.00000000) controlPoint2: BOTTOM_RIGHT(1.08849323, 0.00000000)]; - [path lineToPoint: BOTTOM_LEFT(1.52866483, 0.00000000)]; - [path curveToPoint: BOTTOM_LEFT(0.66993397, 0.06549569) controlPoint1: BOTTOM_LEFT(1.08849299, 0.00000000) controlPoint2: BOTTOM_LEFT(0.86840701, 0.00000000)]; - [path lineToPoint: BOTTOM_LEFT(0.63149399, 0.07491111)]; - [path curveToPoint: BOTTOM_LEFT(0.07491100, 0.63149399) controlPoint1: BOTTOM_LEFT(0.37282401, 0.16905883) controlPoint2: BOTTOM_LEFT(0.16906001, 0.37282392)]; - [path curveToPoint: BOTTOM_LEFT(0.00000000, 1.52866471) controlPoint1: BOTTOM_LEFT(0.00000000, 0.86840689) controlPoint2: BOTTOM_LEFT(0.00000000, 1.08849323)]; - [path lineToPoint: TOP_LEFT(0.00000000, 1.52866483)]; - [path curveToPoint: TOP_LEFT(0.06549600, 0.66993397) controlPoint1: TOP_LEFT(0.00000000, 1.08849299) controlPoint2: TOP_LEFT(0.00000000, 0.86840701)]; - [path lineToPoint: TOP_LEFT(0.07491100, 0.63149399)]; - [path curveToPoint: TOP_LEFT(0.63149399, 0.07491100) controlPoint1: TOP_LEFT(0.16906001, 0.37282401) controlPoint2: TOP_LEFT(0.37282401, 0.16906001)]; - [path curveToPoint: TOP_LEFT(1.52866483, 0.00000000) controlPoint1: TOP_LEFT(0.86840701, 0.00000000) controlPoint2: TOP_LEFT(1.08849299, 0.00000000)]; - [path closePath]; - return path; -} - -@end diff --git a/PrivateFrameworks/CoreUI.framework/Headers b/PrivateFrameworks/CoreUI.framework/Headers new file mode 120000 index 0000000..fc757d7 --- /dev/null +++ b/PrivateFrameworks/CoreUI.framework/Headers @@ -0,0 +1 @@ +Versions/Current/Headers/ \ No newline at end of file diff --git a/PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUICatalog.h b/PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUICatalog.h new file mode 100755 index 0000000..ab1f24d --- /dev/null +++ b/PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUICatalog.h @@ -0,0 +1,54 @@ +#import + +@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 *)imagesWithName:(id)arg1; + +- (NSArray *)allImageNames; +- (NSArray *)appearanceNames; + +@end + diff --git a/PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUINamedImage.h b/PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUINamedImage.h new file mode 100755 index 0000000..7643f7d --- /dev/null +++ b/PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUINamedImage.h @@ -0,0 +1,53 @@ +#import +#import + +@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 diff --git a/PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUINamedLookup.h b/PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUINamedLookup.h new file mode 100755 index 0000000..25dbcf2 --- /dev/null +++ b/PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUINamedLookup.h @@ -0,0 +1,16 @@ +#import + +@class CUIRenditionKey; + +@interface CUINamedLookup: NSObject { + 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 diff --git a/PrivateFrameworks/CoreUI.framework/Versions/Current b/PrivateFrameworks/CoreUI.framework/Versions/Current new file mode 120000 index 0000000..8c7e5a6 --- /dev/null +++ b/PrivateFrameworks/CoreUI.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/PrivateFrameworks/CoreUI.framework/module.modulemap b/PrivateFrameworks/CoreUI.framework/module.modulemap new file mode 100755 index 0000000..c151ffb --- /dev/null +++ b/PrivateFrameworks/CoreUI.framework/module.modulemap @@ -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 * +} diff --git a/ProvisionQL.xcodeproj/project.pbxproj b/ProvisionQL.xcodeproj/project.pbxproj index 3c5e9ef..ab40ae7 100644 --- a/ProvisionQL.xcodeproj/project.pbxproj +++ b/ProvisionQL.xcodeproj/project.pbxproj @@ -7,11 +7,23 @@ objects = { /* Begin PBXBuildFile section */ + 54768CFA2B6D3841007E81D2 /* AppIcon.h in Headers */ = {isa = PBXBuildFile; fileRef = 54768CF82B6D3841007E81D2 /* AppIcon.h */; }; + 54768CFB2B6D3841007E81D2 /* AppIcon.m in Sources */ = {isa = PBXBuildFile; fileRef = 54768CF92B6D3841007E81D2 /* AppIcon.m */; }; + 54768D052B701D6C007E81D2 /* CoreUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54768D042B701D6C007E81D2 /* CoreUI.framework */; }; + 54B1E00B2B6989E7009E654A /* Entitlements.h in Headers */ = {isa = PBXBuildFile; fileRef = 54B1E0092B6989E7009E654A /* Entitlements.h */; }; + 54B1E00C2B6989E7009E654A /* Entitlements.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B1E00A2B6989E7009E654A /* Entitlements.m */; }; + 54F4EB002B6668A50000CE41 /* ZipEntry.h in Headers */ = {isa = PBXBuildFile; fileRef = 54F4EAFC2B6668A50000CE41 /* ZipEntry.h */; }; + 54F4EB012B6668A50000CE41 /* ZipEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = 54F4EAFD2B6668A50000CE41 /* ZipEntry.m */; }; + 54F4EB022B6668A50000CE41 /* pinch.m in Sources */ = {isa = PBXBuildFile; fileRef = 54F4EAFE2B6668A50000CE41 /* pinch.m */; }; + 54F4EB032B6668A50000CE41 /* pinch.h in Headers */ = {isa = PBXBuildFile; fileRef = 54F4EAFF2B6668A50000CE41 /* pinch.h */; }; + 54F4EB0F2B668F7E0000CE41 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 54F4EB0E2B668F7E0000CE41 /* libz.dylib */; }; + 54F4EB222B66D6FE0000CE41 /* ZipFile.h in Headers */ = {isa = PBXBuildFile; fileRef = 54F4EB202B66D6FE0000CE41 /* ZipFile.h */; }; + 54F4EB232B66D6FE0000CE41 /* ZipFile.m in Sources */ = {isa = PBXBuildFile; fileRef = 54F4EB212B66D6FE0000CE41 /* ZipFile.m */; }; + 54F4EB682B6719310000CE41 /* AppCategories.h in Headers */ = {isa = PBXBuildFile; fileRef = 54F4EB662B6719310000CE41 /* AppCategories.h */; }; + 54F4EB692B6719310000CE41 /* AppCategories.m in Sources */ = {isa = PBXBuildFile; fileRef = 54F4EB672B6719310000CE41 /* AppCategories.m */; }; 553C6D321879E457002237FC /* blankIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 553C6D311879E457002237FC /* blankIcon.png */; }; 55424C611870D4AA002F5408 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55424C601870D4AA002F5408 /* AppKit.framework */; }; 55424C631870D90E002F5408 /* defaultIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 55424C621870D90E002F5408 /* defaultIcon.png */; }; - 55424C671870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.h in Headers */ = {isa = PBXBuildFile; fileRef = 55424C651870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.h */; }; - 55424C681870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.m in Sources */ = {isa = PBXBuildFile; fileRef = 55424C661870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.m */; }; 555E9515186E2D67001D406A /* main.c in Sources */ = {isa = PBXBuildFile; fileRef = 555E9512186E2D67001D406A /* main.c */; }; 555E951C186E2DC0001D406A /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 555E9519186E2DC0001D406A /* template.html */; }; 557C842218731FB7008A2A0C /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 557C842118731FB7008A2A0C /* WebKit.framework */; }; @@ -27,11 +39,23 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 54768CF82B6D3841007E81D2 /* AppIcon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppIcon.h; sourceTree = ""; }; + 54768CF92B6D3841007E81D2 /* AppIcon.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppIcon.m; sourceTree = ""; }; + 54768D042B701D6C007E81D2 /* CoreUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = CoreUI.framework; sourceTree = ""; }; + 54B1E0092B6989E7009E654A /* Entitlements.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Entitlements.h; sourceTree = ""; }; + 54B1E00A2B6989E7009E654A /* Entitlements.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Entitlements.m; sourceTree = ""; }; + 54F4EAFC2B6668A50000CE41 /* ZipEntry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ZipEntry.h; sourceTree = ""; }; + 54F4EAFD2B6668A50000CE41 /* ZipEntry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ZipEntry.m; sourceTree = ""; }; + 54F4EAFE2B6668A50000CE41 /* pinch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = pinch.m; sourceTree = ""; }; + 54F4EAFF2B6668A50000CE41 /* pinch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pinch.h; sourceTree = ""; }; + 54F4EB0E2B668F7E0000CE41 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = ../../../../../usr/lib/libz.dylib; sourceTree = ""; }; + 54F4EB202B66D6FE0000CE41 /* ZipFile.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ZipFile.h; sourceTree = ""; }; + 54F4EB212B66D6FE0000CE41 /* ZipFile.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ZipFile.m; sourceTree = ""; }; + 54F4EB662B6719310000CE41 /* AppCategories.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppCategories.h; sourceTree = ""; }; + 54F4EB672B6719310000CE41 /* AppCategories.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppCategories.m; sourceTree = ""; }; 553C6D311879E457002237FC /* blankIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = blankIcon.png; sourceTree = ""; }; 55424C601870D4AA002F5408 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 55424C621870D90E002F5408 /* defaultIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = defaultIcon.png; sourceTree = ""; }; - 55424C651870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSBezierPath+IOS7RoundedRect.h"; sourceTree = ""; }; - 55424C661870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSBezierPath+IOS7RoundedRect.m"; sourceTree = ""; }; 55457C11203C4A9E00ED02E5 /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 55457C12203C4A9E00ED02E5 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 55457C13203C4A9E00ED02E5 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -62,6 +86,8 @@ 55DB729B186E195500CAFEE7 /* Security.framework in Frameworks */, 55DB7287186E193500CAFEE7 /* CoreFoundation.framework in Frameworks */, 55DB7281186E193500CAFEE7 /* QuickLook.framework in Frameworks */, + 54768D052B701D6C007E81D2 /* CoreUI.framework in Frameworks */, + 54F4EB0F2B668F7E0000CE41 /* libz.dylib in Frameworks */, 55DB7285186E193500CAFEE7 /* CoreServices.framework in Frameworks */, 55DB7283186E193500CAFEE7 /* ApplicationServices.framework in Frameworks */, ); @@ -70,13 +96,31 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 55424C641870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect */ = { + 54768CE82B69BB27007E81D2 /* PrivateFrameworks */ = { isa = PBXGroup; children = ( - 55424C651870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.h */, - 55424C661870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.m */, + 54768D042B701D6C007E81D2 /* CoreUI.framework */, ); - path = "NSBezierPath+IOS7RoundedRect"; + path = PrivateFrameworks; + sourceTree = ""; + }; + 54F4EAFB2B6668940000CE41 /* 3rd-party */ = { + isa = PBXGroup; + children = ( + 54F4EB162B669A510000CE41 /* pinch */, + ); + path = "3rd-party"; + sourceTree = ""; + }; + 54F4EB162B669A510000CE41 /* pinch */ = { + isa = PBXGroup; + children = ( + 54F4EAFF2B6668A50000CE41 /* pinch.h */, + 54F4EAFE2B6668A50000CE41 /* pinch.m */, + 54F4EAFC2B6668A50000CE41 /* ZipEntry.h */, + 54F4EAFD2B6668A50000CE41 /* ZipEntry.m */, + ); + path = pinch; sourceTree = ""; }; 55457C10203C4A7500ED02E5 /* Metadata */ = { @@ -119,8 +163,8 @@ 55DB7272186E193500CAFEE7 = { isa = PBXGroup; children = ( + 54768CE82B69BB27007E81D2 /* PrivateFrameworks */, 55457C10203C4A7500ED02E5 /* Metadata */, - 55424C641870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect */, 55DB7288186E193500CAFEE7 /* ProvisionQL */, 55DB727F186E193500CAFEE7 /* Frameworks */, 55DB727E186E193500CAFEE7 /* Products */, @@ -138,6 +182,7 @@ 55DB727F186E193500CAFEE7 /* Frameworks */ = { isa = PBXGroup; children = ( + 54F4EB0E2B668F7E0000CE41 /* libz.dylib */, 557C842118731FB7008A2A0C /* WebKit.framework */, 55424C601870D4AA002F5408 /* AppKit.framework */, 55DB729A186E195500CAFEE7 /* Security.framework */, @@ -152,11 +197,20 @@ 55DB7288186E193500CAFEE7 /* ProvisionQL */ = { isa = PBXGroup; children = ( + 54F4EAFB2B6668940000CE41 /* 3rd-party */, 555E9511186E2D67001D406A /* Supporting-files */, 555E951A186E2DC0001D406A /* Scripts */, 555E9518186E2DC0001D406A /* Resources */, + 54F4EB662B6719310000CE41 /* AppCategories.h */, + 54F4EB672B6719310000CE41 /* AppCategories.m */, + 54768CF82B6D3841007E81D2 /* AppIcon.h */, + 54768CF92B6D3841007E81D2 /* AppIcon.m */, + 54B1E0092B6989E7009E654A /* Entitlements.h */, + 54B1E00A2B6989E7009E654A /* Entitlements.m */, 557C842718733828008A2A0C /* Shared.h */, 557C842418732599008A2A0C /* Shared.m */, + 54F4EB202B66D6FE0000CE41 /* ZipFile.h */, + 54F4EB212B66D6FE0000CE41 /* ZipFile.m */, 55DB728E186E193500CAFEE7 /* GenerateThumbnailForURL.m */, 55DB7290186E193500CAFEE7 /* GeneratePreviewForURL.m */, ); @@ -170,8 +224,13 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 55424C671870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.h in Headers */, + 54F4EB682B6719310000CE41 /* AppCategories.h in Headers */, + 54768CFA2B6D3841007E81D2 /* AppIcon.h in Headers */, + 54B1E00B2B6989E7009E654A /* Entitlements.h in Headers */, + 54F4EB002B6668A50000CE41 /* ZipEntry.h in Headers */, 557C842818733828008A2A0C /* Shared.h in Headers */, + 54F4EB032B6668A50000CE41 /* pinch.h in Headers */, + 54F4EB222B66D6FE0000CE41 /* ZipFile.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -204,7 +263,7 @@ 55DB7273186E193500CAFEE7 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1240; ORGANIZATIONNAME = "Evgeny Aleksandrov"; TargetAttributes = { 55DB727C186E193500CAFEE7 = { @@ -277,11 +336,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 55424C681870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.m in Sources */, + 54768CFB2B6D3841007E81D2 /* AppIcon.m in Sources */, 555E9515186E2D67001D406A /* main.c in Sources */, + 54F4EB012B6668A50000CE41 /* ZipEntry.m in Sources */, + 54F4EB022B6668A50000CE41 /* pinch.m in Sources */, 557C842618732599008A2A0C /* Shared.m in Sources */, 55DB728F186E193500CAFEE7 /* GenerateThumbnailForURL.m in Sources */, + 54F4EB692B6719310000CE41 /* AppCategories.m in Sources */, + 54B1E00C2B6989E7009E654A /* Entitlements.m in Sources */, 55DB7291186E193500CAFEE7 /* GeneratePreviewForURL.m in Sources */, + 54F4EB232B66D6FE0000CE41 /* ZipFile.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -309,6 +373,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -360,6 +425,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -393,12 +459,20 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 5567X9EQ9Q; ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/PrivateFrameworks", + ); INFOPLIST_FILE = "ProvisionQL/Supporting-files/Info.plist"; INSTALL_PATH = /Library/QuickLook; - MARKETING_VERSION = 1.6.4; + MARKETING_VERSION = 1.7.0; OTHER_CODE_SIGN_FLAGS = "--timestamp"; PRODUCT_BUNDLE_IDENTIFIER = com.ealeksandrov.ProvisionQL; PRODUCT_NAME = "$(TARGET_NAME)"; + SYSTEM_FRAMEWORK_SEARCH_PATHS = ( + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + "$(inherited)", + ); WRAPPER_EXTENSION = qlgenerator; }; name = Debug; @@ -413,12 +487,20 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 5567X9EQ9Q; ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/PrivateFrameworks", + ); INFOPLIST_FILE = "ProvisionQL/Supporting-files/Info.plist"; INSTALL_PATH = /Library/QuickLook; - MARKETING_VERSION = 1.6.4; + MARKETING_VERSION = 1.7.0; OTHER_CODE_SIGN_FLAGS = "--timestamp"; PRODUCT_BUNDLE_IDENTIFIER = com.ealeksandrov.ProvisionQL; PRODUCT_NAME = "$(TARGET_NAME)"; + SYSTEM_FRAMEWORK_SEARCH_PATHS = ( + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + "$(inherited)", + ); WRAPPER_EXTENSION = qlgenerator; }; name = Release; diff --git a/ProvisionQL/3rd-party/pinch/ZipEntry.h b/ProvisionQL/3rd-party/pinch/ZipEntry.h new file mode 100755 index 0000000..777c357 --- /dev/null +++ b/ProvisionQL/3rd-party/pinch/ZipEntry.h @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------- + + Modified 2024 by relikd + + Based on original version: + + https://github.com/epatel/pinch-objc + + Copyright (c) 2011-2012 Edward Patel + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + ---------------------------------------------------------------------------*/ + +#import + + +@interface ZipEntry : NSObject { + NSString *url; + NSString *filepath; + int offset; + int method; + int sizeCompressed; + int sizeUncompressed; + unsigned int crc32; + int filenameLength; + int extraFieldLength; + NSData *data; +} + +@property (nonatomic, retain) NSString *url; +@property (nonatomic, retain) NSString *filepath; +@property (nonatomic, assign) int offset; +@property (nonatomic, assign) int method; +@property (nonatomic, assign) int sizeCompressed; +@property (nonatomic, assign) int sizeUncompressed; +@property (nonatomic, assign) unsigned int crc32; +@property (nonatomic, assign) int filenameLength; +@property (nonatomic, assign) int extraFieldLength; +@property (nonatomic, retain) NSData *data; + +@end + +@interface NSArray (ZipEntry) + +- (ZipEntry*)zipEntryWithPath:(NSString*)path; + +@end diff --git a/ProvisionQL/3rd-party/pinch/ZipEntry.m b/ProvisionQL/3rd-party/pinch/ZipEntry.m new file mode 100755 index 0000000..ca54491 --- /dev/null +++ b/ProvisionQL/3rd-party/pinch/ZipEntry.m @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------- + + Modified 2024 by relikd + + Based on original version: + + https://github.com/epatel/pinch-objc + + Copyright (c) 2011-2012 Edward Patel + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + ---------------------------------------------------------------------------*/ + +#import "ZipEntry.h" + + +@implementation ZipEntry + +@synthesize url; +@synthesize filepath; +@synthesize offset; +@synthesize method; +@synthesize sizeCompressed; +@synthesize sizeUncompressed; +@synthesize crc32; +@synthesize filenameLength; +@synthesize extraFieldLength; +@synthesize data; + +@end + +@implementation NSArray (ZipEntry) + +- (ZipEntry*)zipEntryWithPath:(NSString*)path { + NSPredicate *pred = [NSPredicate predicateWithFormat:@"filepath LIKE %@", path]; + return [self filteredArrayUsingPredicate:pred].firstObject; +} + +@end diff --git a/ProvisionQL/3rd-party/pinch/pinch.h b/ProvisionQL/3rd-party/pinch/pinch.h new file mode 100755 index 0000000..5499cc0 --- /dev/null +++ b/ProvisionQL/3rd-party/pinch/pinch.h @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------- + + Modified 2024 by relikd + + Based on original version: + + https://github.com/epatel/pinch-objc + + Copyright (c) 2011-2012 Edward Patel + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + ---------------------------------------------------------------------------*/ + +#import +#import "ZipEntry.h" + +NSData *unzipFileEntry(NSString *path, ZipEntry *entry); +NSArray *listZip(NSString *path); diff --git a/ProvisionQL/3rd-party/pinch/pinch.m b/ProvisionQL/3rd-party/pinch/pinch.m new file mode 100755 index 0000000..f333b44 --- /dev/null +++ b/ProvisionQL/3rd-party/pinch/pinch.m @@ -0,0 +1,329 @@ +/*--------------------------------------------------------------------------- + + Modified 2024 by relikd + + Based on original version: + + https://github.com/epatel/pinch-objc + + Copyright (c) 2011-2012 Edward Patel + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + ---------------------------------------------------------------------------*/ + +#import "pinch.h" +#import "ZipEntry.h" + +#include +#include +#include + +typedef unsigned int uint32; +typedef unsigned short uint16; + +// The headers, see http://en.wikipedia.org/wiki/ZIP_(file_format)#File_headers +// Note that here they will not be as tightly packed as defined in the file format, +// so the extraction is done with a macro below. + +typedef struct ZipRecordEnd { + uint32 endOfCentralDirectorySignature; + uint16 numberOfThisDisk; + uint16 diskWhereCentralDirectoryStarts; + uint16 numberOfCentralDirectoryRecordsOnThisDisk; + uint16 totalNumberOfCentralDirectoryRecords; + uint32 sizeOfCentralDirectory; + uint32 offsetOfStartOfCentralDirectory; + uint16 ZIPfileCommentLength; +} ZipRecordEnd; + +typedef struct ZipRecordDir { + uint32 centralDirectoryFileHeaderSignature; + uint16 versionMadeBy; + uint16 versionNeededToExtract; + uint16 generalPurposeBitFlag; + uint16 compressionMethod; + uint16 fileLastModificationTime; + uint16 fileLastModificationDate; + uint32 CRC32; + uint32 compressedSize; + uint32 uncompressedSize; + uint16 fileNameLength; + uint16 extraFieldLength; + uint16 fileCommentLength; + uint16 diskNumberWhereFileStarts; + uint16 internalFileAttributes; + uint32 externalFileAttributes; + uint32 relativeOffsetOfLocalFileHeader; +} ZipRecordDir; + +typedef struct ZipFileHeader { + uint32 localFileHeaderSignature; + uint16 versionNeededToExtract; + uint16 generalPurposeBitFlag; + uint16 compressionMethod; + uint16 fileLastModificationTime; + uint16 fileLastModificationDate; + uint32 CRC32; + uint32 compressedSize; + uint32 uncompressedSize; + uint16 fileNameLength; + uint16 extraFieldLength; +} ZipFileHeader; + + +BOOL isValid(unsigned char *ptr, int lenUncompressed, uint32 expectedCrc32) { + unsigned long crc = crc32(0L, Z_NULL, 0); + crc = crc32(crc, (const unsigned char*)ptr, lenUncompressed); + BOOL valid = crc == expectedCrc32; + if (!valid) { + NSLog(@"WARN: CRC check failed."); + } + return valid; +} + + +// MARK: - Unzip data + +NSData *unzipFileEntry(NSString *path, ZipEntry *entry) { + NSData *inputData = nil; + NSData *outputData = nil; + int length = sizeof(ZipFileHeader) + entry.sizeCompressed + entry.filenameLength + entry.extraFieldLength; + + // Download '16' extra bytes as I've seen that extraFieldLength sometimes differs + // from the centralDirectory and the fileEntry header... + NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:path]; + @try { + [fp seekToFileOffset:entry.offset]; + inputData = [fp readDataOfLength:length + 16]; + } @finally { + [fp closeFile]; + } + + if (!inputData) + return nil; + + // NSData *data = [NSData new]; + unsigned char *cptr = (unsigned char*)[inputData bytes]; + + ZipFileHeader file_record; + int idx = 0; + + // Extract fields with a macro, if we would need to swap byteorder this would be the place +#define GETFIELD( _field ) \ +memcpy(&file_record._field, &cptr[idx], sizeof(file_record._field)); \ +idx += sizeof(file_record._field) + GETFIELD( localFileHeaderSignature ); + GETFIELD( versionNeededToExtract ); + GETFIELD( generalPurposeBitFlag ); + GETFIELD( compressionMethod ); + GETFIELD( fileLastModificationTime ); + GETFIELD( fileLastModificationDate ); + GETFIELD( CRC32 ); + GETFIELD( compressedSize ); + GETFIELD( uncompressedSize ); + GETFIELD( fileNameLength ); + GETFIELD( extraFieldLength ); +#undef GETFIELD + + if (entry.method == Z_DEFLATED) { + z_stream zstream; + int ret; + + zstream.zalloc = Z_NULL; + zstream.zfree = Z_NULL; + zstream.opaque = Z_NULL; + zstream.avail_in = 0; + zstream.next_in = Z_NULL; + + ret = inflateInit2(&zstream, -MAX_WBITS); + if (ret != Z_OK) + return nil; + + zstream.avail_in = entry.sizeCompressed; + zstream.next_in = &cptr[idx + file_record.fileNameLength + file_record.extraFieldLength]; + + unsigned char *ptr = malloc(entry.sizeUncompressed); + + zstream.avail_out = entry.sizeUncompressed; + zstream.next_out = ptr; + + ret = inflate(&zstream, Z_SYNC_FLUSH); + + if (isValid(ptr, entry.sizeUncompressed, file_record.CRC32)) { + outputData = [NSData dataWithBytes:ptr length:entry.sizeUncompressed]; + } + + free(ptr); + + // TODO: handle inflate errors + assert(ret != Z_STREAM_ERROR); /* state not clobbered */ + switch (ret) { + case Z_NEED_DICT: + ret = Z_DATA_ERROR; /* and fall through */ + case Z_DATA_ERROR: + case Z_MEM_ERROR: + //inflateEnd(&zstream); + //return; + ; + } + + inflateEnd(&zstream); + + } else if (entry.method == 0) { + + unsigned char *ptr = &cptr[idx + file_record.fileNameLength + file_record.extraFieldLength]; + + if (isValid(ptr, entry.sizeUncompressed, file_record.CRC32)) { + outputData = [NSData dataWithBytes:ptr length:entry.sizeUncompressed]; + } + + } else { + NSLog(@"WARN: unimplemented compression method: %d", entry.method); + } + + return outputData; +} + + +// MARK: - List files + +/// Find signature for central directory. +ZipRecordEnd findCentralDirectory(NSFileHandle *fp) { + unsigned long long eof = [fp seekToEndOfFile]; + [fp seekToFileOffset:MAX(0, eof - 4096)]; + NSData *data = [fp readDataToEndOfFile]; + + char centralDirSignature[4] = { + 0x50, 0x4b, 0x05, 0x06 + }; + + const char *cptr = (const char*)[data bytes]; + long len = [data length]; + char *found = NULL; + + do { + char *fptr = memchr(cptr, 0x50, len); + + if (!fptr) // done searching + break; + + // Use the last found directory + if (!memcmp(centralDirSignature, fptr, 4)) + found = fptr; + + len = len - (fptr - cptr) - 1; + cptr = fptr + 1; + } while (1); + + ZipRecordEnd end_record = {}; + if (!found) { + NSLog(@"WARN: no zip end-header found!"); + return end_record; + } + + int idx = 0; + // Extract fields with a macro, if we would need to swap byteorder this would be the place +#define GETFIELD( _field ) \ +memcpy(&end_record._field, &found[idx], sizeof(end_record._field)); \ +idx += sizeof(end_record._field) + GETFIELD( endOfCentralDirectorySignature ); + GETFIELD( numberOfThisDisk ); + GETFIELD( diskWhereCentralDirectoryStarts ); + GETFIELD( numberOfCentralDirectoryRecordsOnThisDisk ); + GETFIELD( totalNumberOfCentralDirectoryRecords ); + GETFIELD( sizeOfCentralDirectory ); + GETFIELD( offsetOfStartOfCentralDirectory ); + GETFIELD( ZIPfileCommentLength ); +#undef GETFIELD + return end_record; +} + +/// List all files and folders of of the central directory. +NSArray *listCentralDirectory(NSFileHandle *fp, ZipRecordEnd end_record) { + [fp seekToFileOffset:end_record.offsetOfStartOfCentralDirectory]; + NSData *data = [fp readDataOfLength:end_record.sizeOfCentralDirectory]; + + const char *cptr = (const char*)[data bytes]; + long len = [data length]; + + // 46 ?!? That's the record length up to the filename see + // http://en.wikipedia.org/wiki/ZIP_(file_format)#File_headers + + NSMutableArray *array = [NSMutableArray array]; + while (len > 46) { + ZipRecordDir dir_record; + int idx = 0; + + // Extract fields with a macro, if we would need to swap byteorder this would be the place +#define GETFIELD( _field ) \ +memcpy(&dir_record._field, &cptr[idx], sizeof(dir_record._field)); \ +idx += sizeof(dir_record._field) + GETFIELD( centralDirectoryFileHeaderSignature ); + GETFIELD( versionMadeBy ); + GETFIELD( versionNeededToExtract ); + GETFIELD( generalPurposeBitFlag ); + GETFIELD( compressionMethod ); + GETFIELD( fileLastModificationTime ); + GETFIELD( fileLastModificationDate ); + GETFIELD( CRC32 ); + GETFIELD( compressedSize ); + GETFIELD( uncompressedSize ); + GETFIELD( fileNameLength ); + GETFIELD( extraFieldLength ); + GETFIELD( fileCommentLength ); + GETFIELD( diskNumberWhereFileStarts ); + GETFIELD( internalFileAttributes ); + GETFIELD( externalFileAttributes ); + GETFIELD( relativeOffsetOfLocalFileHeader ); +#undef GETFIELD + + NSString *filename = [[NSString alloc] initWithBytes:cptr + 46 + length:dir_record.fileNameLength + encoding:NSUTF8StringEncoding]; + ZipEntry *entry = [[ZipEntry alloc] init]; + entry.url = @""; //url + entry.filepath = filename; + entry.method = dir_record.compressionMethod; + entry.sizeCompressed = dir_record.compressedSize; + entry.sizeUncompressed = dir_record.uncompressedSize; + entry.offset = dir_record.relativeOffsetOfLocalFileHeader; + entry.filenameLength = dir_record.fileNameLength; + entry.extraFieldLength = dir_record.extraFieldLength; + [array addObject:entry]; + + len -= 46 + dir_record.fileNameLength + dir_record.extraFieldLength + dir_record.fileCommentLength; + cptr += 46 + dir_record.fileNameLength + dir_record.extraFieldLength + dir_record.fileCommentLength; + } + return array; +} + +NSArray *listZip(NSString *path) { + NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:path]; + @try { + ZipRecordEnd end_record = findCentralDirectory(fp); + if (end_record.sizeOfCentralDirectory == 0) { + return nil; + } + return listCentralDirectory(fp, end_record); + } @finally { + [fp closeFile]; + } + return nil; +} diff --git a/ProvisionQL/AppCategories.h b/ProvisionQL/AppCategories.h new file mode 100644 index 0000000..683f9b0 --- /dev/null +++ b/ProvisionQL/AppCategories.h @@ -0,0 +1,4 @@ +#import +#import + +NSDictionary *getAppCategories(void); diff --git a/ProvisionQL/AppCategories.m b/ProvisionQL/AppCategories.m new file mode 100644 index 0000000..80508da --- /dev/null +++ b/ProvisionQL/AppCategories.m @@ -0,0 +1,163 @@ +#import "AppCategories.h" + +/* + #!/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)) + */ + +NSDictionary *getAppCategories() { + static NSDictionary* categories = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + categories = @{ + // 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" + }; + }); + return categories; +} diff --git a/ProvisionQL/AppIcon.h b/ProvisionQL/AppIcon.h new file mode 100644 index 0000000..d9320c6 --- /dev/null +++ b/ProvisionQL/AppIcon.h @@ -0,0 +1,17 @@ +#import +#import + +#import "Shared.h" + +@interface NSImage (AppIcon) +- (NSImage * _Nonnull)withRoundCorners; +- (NSString * _Nonnull)asBase64; +- (void)downscale:(CGSize)maxSize; +@end + + +@interface AppIcon : NSObject ++ (instancetype _Nonnull)load:(QuickLookInfo)meta; +- (BOOL)canExtractImage; +- (NSImage * _Nonnull)extractImage:(NSDictionary * _Nullable)appPlist; +@end diff --git a/ProvisionQL/AppIcon.m b/ProvisionQL/AppIcon.m new file mode 100644 index 0000000..3825792 --- /dev/null +++ b/ProvisionQL/AppIcon.m @@ -0,0 +1,383 @@ +#import "AppIcon.h" +#import "Shared.h" +#import "ZipEntry.h" + +#define CUI_ENABLED 1 + +#ifdef CUI_ENABLED +#import +#import +#endif + + +@interface AppIcon() +@property (nonatomic, assign) QuickLookInfo meta; +@end + + +@implementation AppIcon + ++ (instancetype)load:(QuickLookInfo)meta { + return [[self alloc] initWithMeta:meta]; +} + +- (instancetype)initWithMeta:(QuickLookInfo)meta { + self = [super init]; + if (self) { + _meta = meta; + } + return self; +} + + +// MARK: - Public methods + +/// You should check this before calling @c extractImage +- (BOOL)canExtractImage { + switch (_meta.type) { + case FileTypeIPA: + case FileTypeArchive: + case FileTypeExtension: + return YES; + case FileTypeProvision: + return NO; + } + return NO; +} + + +// MARK: - Image Extraction + +/// Try multiple methods to extract image. You should check @c canExtractImage before calling this method. +/// This method will always return an image even if none is found, in which case it returns the default image. +- (NSImage * _Nonnull)extractImage:(NSDictionary * _Nullable)appPlist { + // no need to unwrap the plist, and most .ipa should include the Artwork anyway + if (_meta.type == FileTypeIPA) { + NSData *data = [_meta.zipFile unzipFile:@"iTunesArtwork"]; + if (data) { +#ifdef DEBUG + NSLog(@"[icon] using iTunesArtwork."); +#endif + return [[NSImage alloc] initWithData:data]; + } + } + + // Extract image name from app plist + NSString *plistImgName = [self iconNameFromPlist:appPlist]; +#ifdef DEBUG + NSLog(@"[icon] icon name: %@", plistImgName); +#endif + if (plistImgName) { + // First, try if an image file with that name exists. + NSString *actualName = [self expandImageName:plistImgName]; + if (actualName) { +#ifdef DEBUG + NSLog(@"[icon] using plist with key %@ and image file %@", plistImgName, actualName); +#endif + if (_meta.type == FileTypeIPA) { + NSData *data = [_meta.zipFile unzipFile:[@"Payload/*.app/" stringByAppendingString:actualName]]; + return [[NSImage alloc] initWithData:data]; + } + NSURL *basePath = _meta.effectiveUrl ?: _meta.url; + return [[NSImage alloc] initWithContentsOfURL:[basePath URLByAppendingPathComponent:actualName]]; + } + + // Else: try Assets.car +#ifdef CUI_ENABLED + @try { + NSImage *img = [self imageFromAssetsCar:plistImgName]; + if (img) { + return img; + } + } @catch (NSException *exception) { + NSLog(@"ERROR: unknown private framework issue: %@", exception); + } +#endif + } + + // Fallback to default icon + NSURL *iconURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"defaultIcon" withExtension:@"png"]; + return [[NSImage alloc] initWithContentsOfURL:iconURL]; +} + +#ifdef CUI_ENABLED + +/// Use @c CUICatalog to extract an image from @c Assets.car +- (NSImage * _Nullable)imageFromAssetsCar:(NSString *)imageName { + NSData *data = readPayloadFile(_meta, @"Assets.car"); + if (!data) { + return nil; + } + NSError *err; + CUICatalog *catalog = [[CUICatalog alloc] initWithBytes:[data bytes] length:data.length error:&err]; + if (err) { + NSLog(@"[icon-car] ERROR: could not open catalog: %@", err); + return nil; + } + NSString *validName = [self carVerifyNameExists:imageName inCatalog:catalog]; + if (validName) { + CUINamedImage *bestImage = [self carFindHighestResolutionIcon:[catalog imagesWithName:validName]]; + if (bestImage) { +#ifdef DEBUG + NSLog(@"[icon] using Assets.car with key %@", validName); +#endif + return [[NSImage alloc] initWithCGImage:bestImage.image size:bestImage.size]; + } + } + return nil; +} + + +// MARK: - Helper: Assets.car + +/// Helper method to check available icon names. Will return a valid name or @c nil if no image with that key is found. +- (NSString * _Nullable)carVerifyNameExists:(NSString *)imageName inCatalog:(CUICatalog *)catalog { + NSArray *availableNames = nil; + @try { + availableNames = [catalog allImageNames]; + } @catch (NSException *exception) { + NSLog(@"[icon-car] ERROR: method allImageNames unavailable: %@", exception); + // fallback to use the provided imageName just in case it may still proceed. + } + if (availableNames && ![availableNames containsObject:imageName]) { + // Theoretically this should never happen. Assuming the image name is found in an image file. + NSLog(@"[icon-car] WARN: key '%@' does not match any available key", imageName); + NSString *alternativeName = [self carSearchAlternativeName:imageName inAvailable:availableNames]; + if (alternativeName) { + NSLog(@"[icon-car] falling back to '%@'", alternativeName); + return alternativeName; + } + // NSLog(@"[icon-car] available keys: %@", [car 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" +- (NSString * _Nullable)carSearchAlternativeName:(NSString *)originalName inAvailable:(NSArray *)availableNames { + NSString *bestOption = nil; + NSUInteger bestDiff = 999; + for (NSString *option in availableNames) { + if ([option hasPrefix:originalName] || [originalName hasPrefix:option]) { + NSUInteger thisDiff = MAX(originalName.length, option.length) - MIN(originalName.length, option.length); + if (thisDiff < bestDiff) { + bestDiff = thisDiff; + bestOption = option; + } + } + } + return bestOption; +} + +/// Given a list of @c CUINamedImage, return the one with the highest resolution. Vector graphics are ignored. +- (CUINamedImage * _Nullable)carFindHighestResolutionIcon:(NSArray *)availableImages { + CGFloat largestWidth = 0; + CUINamedImage *largestImage = nil; + for (CUINamedImage *img in availableImages) { + if (![img isKindOfClass:[CUINamedImage class]]) { + continue; // ignore CUINamedMultisizeImageSet + } + @try { + CGFloat w = img.size.width; + if (w > largestWidth) { + largestWidth = w; + largestImage = img; + } + } @catch (NSException *exception) { + continue; + } + } + return largestImage; +} + +#endif + + +// MARK: - Helper: Plist Filename + +/// Parse app plist to find the bundle icon filename. +/// @param appPlist If @c nil, will load plist on the fly (used for thumbnail) +/// @return Filename which is available in Bundle or Filesystem. This may include @c @2x and an arbitrary file extension. +- (NSString * _Nullable)iconNameFromPlist:(NSDictionary *)appPlist { + if (!appPlist) { + appPlist = readPlistApp(_meta); + } + //Check for CFBundleIcons (since 5.0) + NSArray *icons = [self unpackNameListFromPlistDict:appPlist[@"CFBundleIcons"]]; + if (!icons) { + icons = [self unpackNameListFromPlistDict:appPlist[@"CFBundleIcons~ipad"]]; + if (!icons) { + //Check for CFBundleIconFiles (since 3.2) + icons = arrayOrNil(appPlist[@"CFBundleIconFiles"]); + if (!icons) { + icons = arrayOrNil(appPlist[@"Icon files"]); // key found on iTunesU app + if (!icons) { + //Check for CFBundleIconFile (legacy, before 3.2) + return appPlist[@"CFBundleIconFile"]; // may be nil + } + } + } + } + return [self findHighestResolutionIconName:icons]; +} + +/// Given a filename, search Bundle or Filesystem for files that match. Select the filename with the highest resolution. +- (NSString * _Nullable)expandImageName:(NSString * _Nullable)fileName { + if (!fileName) { + return nil; + } + NSArray *matchingNames = nil; + if (_meta.type == FileTypeIPA) { + if (!_meta.zipFile) { + // in case unzip in memory is not available, fallback to pattern matching with dynamic suffix + return [fileName stringByAppendingString:@"*"]; + } + NSString *zipPath = [NSString stringWithFormat:@"Payload/*.app/%@*", fileName]; + NSMutableArray *matches = [NSMutableArray array]; + for (ZipEntry *zip in [_meta.zipFile filesMatching:zipPath]) { + [matches addObject:[zip.filepath lastPathComponent]]; + } + matchingNames = matches; + } else if (_meta.type == FileTypeArchive || _meta.type == FileTypeExtension) { + NSURL *basePath = _meta.effectiveUrl ?: _meta.url; + NSArray *appContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:basePath.path error:nil]; + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF beginswith %@", fileName]; + matchingNames = [appContents filteredArrayUsingPredicate:predicate]; + } + if (matchingNames.count > 0) { + return [self findHighestResolutionIconName:matchingNames]; + } + return nil; +} + +/// Deep select icons from plist key @c CFBundleIcons and @c CFBundleIcons~ipad +- (NSArray * _Nullable)unpackNameListFromPlistDict:(NSDictionary *)bundleDict { + if ([bundleDict isKindOfClass:[NSDictionary class]]) { + NSDictionary *primaryDict = [bundleDict objectForKey:@"CFBundlePrimaryIcon"]; + if ([primaryDict isKindOfClass:[NSDictionary class]]) { + NSArray *icons = [primaryDict objectForKey:@"CFBundleIconFiles"]; + if ([icons isKindOfClass:[NSArray class]]) { + return icons; + } + NSString *name = [primaryDict objectForKey:@"CFBundleIconName"]; // key found on a .tipa file + if ([name isKindOfClass:[NSString class]]) { + return @[name]; + } + } + } + return nil; +} + +/// Given a list of filenames, try to find the one with the highest resolution +- (NSString *)findHighestResolutionIconName:(NSArray *)icons { + for (NSString *match in @[@"@3x", @"@2x", @"180", @"167", @"152", @"120"]) { + for (NSString *icon in icons) { + if ([icon containsString:match]) { + return icon; + } + } + } + //If no one matches any pattern, just take last item + NSString *lastName = [icons lastObject]; + if ([[lastName lowercaseString] containsString:@"small"]) { + return [icons firstObject]; + } + return lastName; +} + +@end + + +// MARK: - Extension: 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. +// + +@implementation NSBezierPath (IOS7RoundedRect) + +#define TOP_LEFT(X, Y) NSMakePoint(rect.origin.x + X * limitedRadius, rect.origin.y + Y * limitedRadius) +#define TOP_RIGHT(X, Y) NSMakePoint(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + Y * limitedRadius) +#define BOTTOM_RIGHT(X, Y) NSMakePoint(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) +#define BOTTOM_LEFT(X, Y) NSMakePoint(rect.origin.x + X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) + +/// iOS 7 rounded corners ++ (NSBezierPath *)bezierPathWithIOS7RoundedRect:(NSRect)rect cornerRadius:(CGFloat)radius { + NSBezierPath *path = NSBezierPath.bezierPath; + CGFloat limit = MIN(rect.size.width, rect.size.height) / 2 / 1.52866483; + CGFloat limitedRadius = MIN(radius, limit); + + [path moveToPoint: TOP_LEFT(1.52866483, 0.00000000)]; + [path lineToPoint: TOP_RIGHT(1.52866471, 0.00000000)]; + [path curveToPoint: TOP_RIGHT(0.66993427, 0.06549600) controlPoint1: TOP_RIGHT(1.08849323, 0.00000000) controlPoint2: TOP_RIGHT(0.86840689, 0.00000000)]; + [path lineToPoint: TOP_RIGHT(0.63149399, 0.07491100)]; + [path curveToPoint: TOP_RIGHT(0.07491176, 0.63149399) controlPoint1: TOP_RIGHT(0.37282392, 0.16905899) controlPoint2: TOP_RIGHT(0.16906013, 0.37282401)]; + [path curveToPoint: TOP_RIGHT(0.00000000, 1.52866483) controlPoint1: TOP_RIGHT(0.00000000, 0.86840701) controlPoint2: TOP_RIGHT(0.00000000, 1.08849299)]; + [path lineToPoint: BOTTOM_RIGHT(0.00000000, 1.52866471)]; + [path curveToPoint: BOTTOM_RIGHT(0.06549569, 0.66993493) controlPoint1: BOTTOM_RIGHT(0.00000000, 1.08849323) controlPoint2: BOTTOM_RIGHT(0.00000000, 0.86840689)]; + [path lineToPoint: BOTTOM_RIGHT(0.07491111, 0.63149399)]; + [path curveToPoint: BOTTOM_RIGHT(0.63149399, 0.07491111) controlPoint1: BOTTOM_RIGHT(0.16905883, 0.37282392) controlPoint2: BOTTOM_RIGHT(0.37282392, 0.16905883)]; + [path curveToPoint: BOTTOM_RIGHT(1.52866471, 0.00000000) controlPoint1: BOTTOM_RIGHT(0.86840689, 0.00000000) controlPoint2: BOTTOM_RIGHT(1.08849323, 0.00000000)]; + [path lineToPoint: BOTTOM_LEFT(1.52866483, 0.00000000)]; + [path curveToPoint: BOTTOM_LEFT(0.66993397, 0.06549569) controlPoint1: BOTTOM_LEFT(1.08849299, 0.00000000) controlPoint2: BOTTOM_LEFT(0.86840701, 0.00000000)]; + [path lineToPoint: BOTTOM_LEFT(0.63149399, 0.07491111)]; + [path curveToPoint: BOTTOM_LEFT(0.07491100, 0.63149399) controlPoint1: BOTTOM_LEFT(0.37282401, 0.16905883) controlPoint2: BOTTOM_LEFT(0.16906001, 0.37282392)]; + [path curveToPoint: BOTTOM_LEFT(0.00000000, 1.52866471) controlPoint1: BOTTOM_LEFT(0.00000000, 0.86840689) controlPoint2: BOTTOM_LEFT(0.00000000, 1.08849323)]; + [path lineToPoint: TOP_LEFT(0.00000000, 1.52866483)]; + [path curveToPoint: TOP_LEFT(0.06549600, 0.66993397) controlPoint1: TOP_LEFT(0.00000000, 1.08849299) controlPoint2: TOP_LEFT(0.00000000, 0.86840701)]; + [path lineToPoint: TOP_LEFT(0.07491100, 0.63149399)]; + [path curveToPoint: TOP_LEFT(0.63149399, 0.07491100) controlPoint1: TOP_LEFT(0.16906001, 0.37282401) controlPoint2: TOP_LEFT(0.37282401, 0.16906001)]; + [path curveToPoint: TOP_LEFT(1.52866483, 0.00000000) controlPoint1: TOP_LEFT(0.86840701, 0.00000000) controlPoint2: TOP_LEFT(1.08849299, 0.00000000)]; + [path closePath]; + return path; +} + +@end + + +// MARK: - Extension: NSImage + + +@implementation NSImage (AppIcon) + +/// Apply rounded corners to image (iOS7 style) +- (NSImage * _Nonnull)withRoundCorners { + NSSize existingSize = [self size]; + NSImage *composedImage = [[NSImage alloc] initWithSize:existingSize]; + + [composedImage lockFocus]; + [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh]; + + NSRect imageFrame = NSRectFromCGRect(CGRectMake(0, 0, existingSize.width, existingSize.height)); + NSBezierPath *clipPath = [NSBezierPath bezierPathWithIOS7RoundedRect:imageFrame cornerRadius:existingSize.width * 0.225]; + [clipPath setWindingRule:NSWindingRuleEvenOdd]; + [clipPath addClip]; + + [self drawAtPoint:NSZeroPoint fromRect:NSMakeRect(0, 0, existingSize.width, existingSize.height) operation:NSCompositingOperationSourceOver fraction:1]; + [composedImage unlockFocus]; + return composedImage; +} + +/// Convert image to PNG and encode with base64 to be embeded in html output. +- (NSString * _Nonnull)asBase64 { + // appIcon = [self roundCorners:appIcon]; + NSData *imageData = [self TIFFRepresentation]; + NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData]; + imageData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; + return [imageData base64EncodedStringWithOptions:0]; +} + +/// If the image is larger than the provided maximum size, scale it down. Otherwise leave it untouched. +- (void)downscale:(CGSize)maxSize { + // TODO: if downscale, then this should respect retina resolution + if (self.size.width > maxSize.width && self.size.height > maxSize.height) { + [self setSize:maxSize]; + } +} + +@end diff --git a/ProvisionQL/Entitlements.h b/ProvisionQL/Entitlements.h new file mode 100644 index 0000000..576966e --- /dev/null +++ b/ProvisionQL/Entitlements.h @@ -0,0 +1,17 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface Entitlements : NSObject +@property (nonatomic, assign, readonly) BOOL hasError; +/// only set after calling @c applyFallbackIfNeeded: +@property (nonatomic, retain, readonly) NSString * _Nullable html; + ++ (instancetype)withoutBinary; ++ (instancetype)withBinary:(NSString *)appBinaryPath; +- (instancetype)init UNAVAILABLE_ATTRIBUTE; + +- (void)applyFallbackIfNeeded:(NSDictionary * _Nullable)fallbackEntitlementsPlist; +@end + +NS_ASSUME_NONNULL_END diff --git a/ProvisionQL/Entitlements.m b/ProvisionQL/Entitlements.m new file mode 100644 index 0000000..89f085f --- /dev/null +++ b/ProvisionQL/Entitlements.m @@ -0,0 +1,212 @@ +#import "Entitlements.h" + +void recursiveKeyValue(NSUInteger level, NSString *key, id value, NSMutableString *output); + + +@interface Entitlements() +@property (nonatomic, copy, readonly) NSString * _Nonnull binaryPath; +/// It is either @c plist or @c codeSignErrors not both. +@property (nonatomic, retain, readonly) NSDictionary * _Nullable plist; +/// It is either @c plist or @c codeSignErrors not both. +@property (nonatomic, retain, readonly) NSString * _Nullable codeSignError; +@end + + +@implementation Entitlements + +/// Use provision plist data without running @c codesign or ++ (instancetype)withoutBinary { + return [[self alloc] init]; +} + +/// First, try to extract real entitlements by running @c SecCode module in-memory. +/// If that fails, fallback to running @c codesign via system call. ++ (instancetype)withBinary:(NSString * _Nonnull)appBinaryPath { + return [[self alloc] initWithBinaryPath:appBinaryPath]; +} + +- (instancetype)initWithBinaryPath:(NSString * _Nonnull)path { + self = [super init]; + if (self) { + _binaryPath = path; + _plist = [self getSecCodeEntitlements]; + if (!_plist) { + _plist = [self sysCallCodeSign]; // fallback to system call + } + } + return self; +} + +// MARK: - public methods + +/// Provided provision plist is only used if @c SecCode and @c CodeSign failed. +- (void)applyFallbackIfNeeded:(NSDictionary * _Nullable)fallbackEntitlementsPlist { + // checking for !error ensures that codesign gets precedence. + // show error before falling back to provision based entitlements. + if (!_plist && !_codeSignError) { + // read the entitlements from the provisioning profile instead + if ([fallbackEntitlementsPlist isKindOfClass:[NSDictionary class]]) { +#ifdef DEBUG + NSLog(@"[entitlements] fallback to provision plist entitlements"); +#endif + _plist = fallbackEntitlementsPlist; + } + } + _html = [self format:_plist]; + _plist = nil; // free memory + _codeSignError = nil; +} + +/// Print formatted plist in a @c \
 tag
+- (NSString * _Nullable)format:(NSDictionary *)plist {
+	if (plist) {
+		NSMutableString *output = [NSMutableString string];
+		recursiveKeyValue(0, nil, plist, output);
+		return [NSString stringWithFormat:@"
%@
", output]; + } + return _codeSignError; // may be nil +} + + +// MARK: - SecCode in-memory reader + +/// use in-memory @c SecCode for entitlement extraction +- (NSDictionary *)getSecCodeEntitlements { + NSURL *url = [NSURL fileURLWithPath:_binaryPath]; + NSDictionary *plist = nil; + SecStaticCodeRef codeRef; + SecStaticCodeCreateWithPath((__bridge CFURLRef)url, kSecCSDefaultFlags, &codeRef); + if (codeRef) { + CFDictionaryRef requirementInfo; + SecCodeCopySigningInformation(codeRef, kSecCSRequirementInformation, &requirementInfo); + if (requirementInfo) { +#ifdef DEBUG + NSLog(@"[entitlements] read SecCode 'entitlements-dict' key"); +#endif + CFDictionaryRef dict = CFDictionaryGetValue(requirementInfo, kSecCodeInfoEntitlementsDict); + // if 'entitlements-dict' key exists, use that one + if (dict) { + plist = (__bridge NSDictionary *)dict; + } + // else, fallback to parse data from 'entitlements' key + if (!plist) { +#ifdef DEBUG + NSLog(@"[entitlements] read SecCode 'entitlements' key"); +#endif + NSData *data = (__bridge NSData*)CFDictionaryGetValue(requirementInfo, kSecCodeInfoEntitlements); + if (data) { + NSData *header = [data subdataWithRange:NSMakeRange(0, 8)]; + const char *cptr = (const char*)[header bytes]; + + // expected magic header number. Currently no support for other formats. + if (memcmp("\xFA\xDE\x71\x71", cptr, 4) == 0) { + // big endian, so no memcpy for us :( + uint32_t size = ((uint8_t)cptr[4] << 24) | ((uint8_t)cptr[5] << 16) | ((uint8_t)cptr[6] << 8) | (uint8_t)cptr[7]; + if (size == data.length) { + data = [data subdataWithRange:NSMakeRange(8, data.length - 8)]; + } else { + NSLog(@"[entitlements] unpack error for FADE7171 size %lu != %u", data.length, size); + } + } else { + NSLog(@"[entitlements] unsupported embedded plist format: %@", header); + } + plist = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL]; + } + } + CFRelease(requirementInfo); + } + CFRelease(codeRef); + } + return plist; +} + + +// MARK: - fallback to sys call + +/// run: @c codesign -d --entitlements - --xml +- (NSDictionary *)sysCallCodeSign { + NSTask *codesignTask = [NSTask new]; + [codesignTask setLaunchPath:@"/usr/bin/codesign"]; + [codesignTask setStandardOutput:[NSPipe pipe]]; + [codesignTask setStandardError:[NSPipe pipe]]; + if (@available(macOS 11, *)) { + [codesignTask setArguments:@[@"-d", _binaryPath, @"--entitlements", @"-", @"--xml"]]; + } else { + [codesignTask setArguments:@[@"-d", _binaryPath, @"--entitlements", @":-"]]; + } + [codesignTask launch]; + +#ifdef DEBUG + NSLog(@"[sys-call] codesign %@", [[codesignTask arguments] componentsJoinedByString:@" "]); +#endif + + NSData *outputData = [[[codesignTask standardOutput] fileHandleForReading] readDataToEndOfFile]; + NSData *errorData = [[[codesignTask standardError] fileHandleForReading] readDataToEndOfFile]; + [codesignTask waitUntilExit]; + + if (outputData) { + NSDictionary *plist = [NSPropertyListSerialization propertyListWithData:outputData options:0 format:NULL error:NULL]; + if (plist) { + return plist; + } + // errorData = outputData; // not sure if necessary + } + + NSString *output = [[NSString alloc] initWithData:errorData ?: outputData encoding:NSUTF8StringEncoding]; + if ([output hasPrefix:@"Executable="]) { + // remove first line with long temporary path to the executable + NSArray *allLines = [output componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + _codeSignError = [[allLines subarrayWithRange:NSMakeRange(1, allLines.count - 1)] componentsJoinedByString:@"
"]; + } else { + _codeSignError = output; + } + _hasError = YES; + return nil; +} + +@end + + +// MARK: - Plist formatter + +/// Print recursive tree of key-value mappings. +void recursiveKeyValue(NSUInteger level, NSString *key, id value, NSMutableString *output) { + int indent = (int)(level * 4); + + if ([value isKindOfClass:[NSDictionary class]]) { + if (key) { + [output appendFormat:@"%*s%@ = {\n", indent, "", key]; + } else if (level != 0) { + [output appendFormat:@"%*s{\n", indent, ""]; + } + NSDictionary *dictionary = (NSDictionary *)value; + NSArray *keys = [[dictionary allKeys] sortedArrayUsingSelector:@selector(compare:)]; + for (NSString *subKey in keys) { + NSUInteger subLevel = (key == nil && level == 0) ? 0 : level + 1; + recursiveKeyValue(subLevel, subKey, [dictionary valueForKey:subKey], output); + } + if (level != 0) { + [output appendFormat:@"%*s}\n", indent, ""]; + } + } else if ([value isKindOfClass:[NSArray class]]) { + [output appendFormat:@"%*s%@ = (\n", indent, "", key]; + NSArray *array = (NSArray *)value; + for (id value in array) { + recursiveKeyValue(level + 1, nil, value, output); + } + [output appendFormat:@"%*s)\n", indent, ""]; + } else if ([value isKindOfClass:[NSData class]]) { + NSData *data = (NSData *)value; + if (key) { + [output appendFormat:@"%*s%@ = %zd bytes of data\n", indent, "", key, [data length]]; + } else { + [output appendFormat:@"%*s%zd bytes of data\n", indent, "", [data length]]; + } + } else { + if (key) { + [output appendFormat:@"%*s%@ = %@\n", indent, "", key, value]; + } else { + [output appendFormat:@"%*s%@\n", indent, "", value]; + } + } +} diff --git a/ProvisionQL/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index 357d56b..c636931 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -1,4 +1,10 @@ #import "Shared.h" +#import "AppCategories.h" +#import "AppIcon.h" +#import "Entitlements.h" + +// makro to stop further processing +#define ALLOW_EXIT if (QLPreviewRequestIsCancelled(preview)) { return noErr; } OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options); void CancelPreviewGeneration(void *thisInterface, QLPreviewRequestRef preview); @@ -9,730 +15,632 @@ This function's job is to create preview for designated file ----------------------------------------------------------------------------- */ -void displayKeyAndValue(NSUInteger level, NSString *key, id value, NSMutableString *output) { - int indent = (int)(level * 4); +// MARK: - Generic data formatting & printing - if ([value isKindOfClass:[NSDictionary class]]) { - if (key) { - [output appendFormat:@"%*s%@ = {\n", indent, "", key]; - } else if (level != 0) { - [output appendFormat:@"%*s{\n", indent, ""]; +typedef NSArray TableRow; + +/// Print html table with arbitrary number of columns +/// @param header If set, start the table with a @c tr column row. +NSString * _Nonnull formatAsTable(TableRow * _Nullable header, NSArray* data) { + NSMutableString *table = [NSMutableString string]; + [table appendString:@"\n"]; + if (header) { + [table appendFormat:@"\n", [header componentsJoinedByString:@"\n", [row componentsJoinedByString:@"
%@
"]]; + } + for (TableRow *row in data) { + [table appendFormat:@"
%@
"]]; + } + [table appendString:@"
\n"]; + return table; +} + +/// Print recursive tree of key-value mappings. +void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *replacements, int level, NSMutableString *output) { + for (NSString *key in dictionary) { + NSString *localizedKey = replacements[key] ?: key; + NSObject *object = dictionary[key]; + + for (int idx = 0; idx < level; idx++) { + [output appendString:(level == 1) ? @"- " : @"  "]; } - NSDictionary *dictionary = (NSDictionary *)value; - NSArray *keys = [[dictionary allKeys] sortedArrayUsingSelector:@selector(compare:)]; - for (NSString *subKey in keys) { - NSUInteger subLevel = (key == nil && level == 0) ? 0 : level + 1; - displayKeyAndValue(subLevel, subKey, [dictionary valueForKey:subKey], output); + + if ([object isKindOfClass:[NSDictionary class]]) { + [output appendFormat:@"%@:
", localizedKey]; + recursiveDictWithReplacements((NSDictionary *)object, replacements, level + 1, output); + [output appendString:@"
"]; + } else if ([object isKindOfClass:[NSNumber class]]) { + object = [(NSNumber *)object boolValue] ? @"YES" : @"NO"; + [output appendFormat:@"%@: %@
", localizedKey, object]; + } else { + [output appendFormat:@"%@: %@
", localizedKey, object]; } - if (level != 0) { - [output appendFormat:@"%*s}\n", indent, ""]; - } - } else if ([value isKindOfClass:[NSArray class]]) { - [output appendFormat:@"%*s%@ = (\n", indent, "", key]; - NSArray *array = (NSArray *)value; - for (id value in array) { - displayKeyAndValue(level + 1, nil, value, output); + } +} + +/// Replace occurrences of chars @c &"'<> with html encoding. +NSString *escapedXML(NSString *stringToEscape) { + stringToEscape = [stringToEscape stringByReplacingOccurrencesOfString:@"&" withString:@"&"]; + NSDictionary *htmlEntityReplacement = @{ + @"\"": @""", + @"'": @"'", + @"<": @"<", + @">": @">", + }; + for (NSString *key in [htmlEntityReplacement allKeys]) { + NSString *replacement = [htmlEntityReplacement objectForKey:key]; + stringToEscape = [stringToEscape stringByReplacingOccurrencesOfString:key withString:replacement]; + } + return stringToEscape; +} + + +// MARK: - Date processing + +/// @return Difference between two dates as components. +NSDateComponents * _Nonnull dateDiff(NSDate *start, NSDate *end, NSCalendar *calendar) { + return [calendar components:(NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute) + fromDate:start toDate:end options:0]; +} + +/// @return Print largest component. E.g., "3 days" or "14 hours" +NSString * _Nonnull relativeDateString(NSDateComponents *comp) { + NSDateComponentsFormatter *formatter = [[NSDateComponentsFormatter alloc] init]; + formatter.unitsStyle = NSDateComponentsFormatterUnitsStyleFull; + formatter.maximumUnitCount = 1; + return [formatter stringFromDateComponents:comp]; +} + +/// @return Print the date with current locale and medium length style. +NSString * _Nonnull formattedDate(NSDate *date) { + NSDateFormatter *formatter = [NSDateFormatter new]; + [formatter setDateStyle:NSDateFormatterMediumStyle]; + [formatter setTimeStyle:NSDateFormatterMediumStyle]; + return [formatter stringFromDate:date]; +} + +/// Parse date from plist regardless if it has @c NSDate or @c NSString type. +NSDate *parseDate(id value) { + if (!value) { + return nil; + } + if ([value isKindOfClass:[NSDate class]]) { + return value; + } + // parse the date from a string + NSString *dateStr = [value description]; + NSDateFormatter *dateFormatter = [NSDateFormatter new]; + [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"]; + NSDate *rv = [dateFormatter dateFromString:dateStr]; + if (!rv) { + [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"]; + rv = [dateFormatter dateFromString:dateStr]; + } + if (!rv) { + NSLog(@"ERROR formatting date: %@", dateStr); + } + return rv; +} + +/// @return Relative distance to today. E.g., "Expired today" +NSString * _Nullable relativeExpirationDateString(NSDate *date) { + if (!date) { + return nil; + } + + NSCalendar *calendar = [NSCalendar currentCalendar]; + BOOL isPast = [date compare:[NSDate date]] == NSOrderedAscending; + BOOL isToday = [calendar isDate:date inSameDayAsDate:[NSDate date]]; + + if (isToday) { + return isPast ? @"Expired today" : @"Expires today"; + } + + if (isPast) { + NSDateComponents *comp = dateDiff(date, [NSDate date], calendar); + return [NSString stringWithFormat:@"Expired %@ ago", relativeDateString(comp)]; + } + + NSDateComponents *comp = dateDiff([NSDate date], date, calendar); + if (comp.day < 30) { + return [NSString stringWithFormat:@"Expires in %@", relativeDateString(comp)]; + } + return [NSString stringWithFormat:@"Expires in %@", relativeDateString(comp)]; +} + +/// @return Relative distance to today. E.g., "DATE (Expires in 3 days)" +NSString * _Nonnull formattedExpirationDate(NSDate *expireDate) { + return [NSString stringWithFormat:@"%@ (%@)", formattedDate(expireDate), relativeExpirationDateString(expireDate)]; +} + +/// @return Relative distance to today. E.g., "DATE (Created 3 days ago)" +NSString * _Nonnull formattedCreationDate(NSDate *creationDate) { + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSDateComponents *comp = dateDiff(creationDate, [NSDate date], calendar); + BOOL isToday = [calendar isDate:creationDate inSameDayAsDate:[NSDate date]]; + return [NSString stringWithFormat:@"%@ (Created %@)", formattedDate(creationDate), + isToday ? @"today" : [NSString stringWithFormat:@"%@ ago", relativeDateString(comp)]]; +} + +/// @return CSS class for expiration status. +NSString * _Nonnull classNameForExpirationStatus(NSDate *date) { + switch (expirationStatus(date)) { + case ExpirationStatusExpired: return @"expired"; + case ExpirationStatusExpiring: return @"expiring"; + case ExpirationStatusValid: return @"valid"; + } +} + + +// MARK: - App Info + +/// @return List of ATS flags. +NSString * _Nonnull formattedAppTransportSecurity(NSDictionary *appPlist) { + NSDictionary *value = appPlist[@"NSAppTransportSecurity"]; + if ([value isKindOfClass:[NSDictionary class]]) { + NSDictionary *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" + }; + + NSMutableString *output = [NSMutableString string]; + recursiveDictWithReplacements(value, localizedKeys, 0, output); + return [NSString stringWithFormat:@"
%@
", output]; + } + + NSString *sdkName = appPlist[@"DTSDKName"]; + double sdkNumber = [[sdkName stringByTrimmingCharactersInSet:[NSCharacterSet letterCharacterSet]] doubleValue]; + if (sdkNumber < 9.0) { + return @"Not applicable before iOS 9.0"; + } + return @"No exceptions"; +} + +/// Process info stored in @c Info.plist +NSDictionary * _Nonnull procAppInfo(NSDictionary *appPlist) { + if (!appPlist) { + return @{ + @"AppInfoHidden": @"hiddenDiv", + @"ProvisionTitleHidden": @"", + }; + } + + NSString *extensionType = appPlist[@"NSExtension"][@"NSExtensionPointIdentifier"]; + + NSMutableArray *platforms = [NSMutableArray array]; + for (NSNumber *number in appPlist[@"UIDeviceFamily"]) { + switch ([number intValue]) { + case 1: [platforms addObject:@"iPhone"]; break; + case 2: [platforms addObject:@"iPad"]; break; + case 3: [platforms addObject:@"TV"]; break; + case 4: [platforms addObject:@"Watch"]; break; + default: break; } - [output appendFormat:@"%*s)\n", indent, ""]; - } else if ([value isKindOfClass:[NSData class]]) { - NSData *data = (NSData *)value; - if (key) { - [output appendFormat:@"%*s%@ = %zd bytes of data\n", indent, "", key, [data length]]; + } + + return @{ + @"AppInfoHidden": @"", + @"ProvisionTitleHidden": @"hiddenDiv", + + @"CFBundleName": appPlist[@"CFBundleDisplayName"] ?: appPlist[@"CFBundleName"] ?: @"", + @"CFBundleShortVersionString": appPlist[@"CFBundleShortVersionString"] ?: @"", + @"CFBundleVersion": appPlist[@"CFBundleVersion"] ?: @"", + @"CFBundleIdentifier": appPlist[@"CFBundleIdentifier"] ?: @"", + + @"ExtensionTypeHidden": extensionType ? @"" : @"hiddenDiv", + @"ExtensionType": extensionType ?: @"", + + @"UIDeviceFamily": [platforms componentsJoinedByString:@", "], + @"DTSDKName": appPlist[@"DTSDKName"] ?: @"", + @"MinimumOSVersion": appPlist[@"MinimumOSVersion"] ?: @"", + @"AppTransportSecurityFormatted": formattedAppTransportSecurity(appPlist), + }; +} + + +// MARK: - iTunes Purchase Information + +/// Concatenate all (sub)genres into a comma separated list. +NSString *formattedGenres(NSDictionary *itunesPlist) { + NSDictionary *categories = getAppCategories(); + NSMutableArray *genres = [NSMutableArray array]; + NSString *mainGenre = categories[itunesPlist[@"genreId"] ?: @0] ?: itunesPlist[@"genre"]; + if (mainGenre) { + [genres addObject:mainGenre]; + } + for (NSDictionary *item in itunesPlist[@"subgenres"]) { + NSString *subgenre = categories[item[@"genreId"] ?: @0] ?: item[@"genre"]; + if (subgenre) { + [genres addObject:subgenre]; + } + } + return [genres componentsJoinedByString:@", "]; +} + +/// Process info stored in @c iTunesMetadata.plist +NSDictionary *parseItunesMeta(NSDictionary *itunesPlist) { + if (!itunesPlist) { + return @{ + @"iTunesHidden": @"hiddenDiv", + }; + } + + NSDictionary *downloadInfo = itunesPlist[@"com.apple.iTunesStore.downloadInfo"]; + NSDictionary *accountInfo = downloadInfo[@"accountInfo"]; + + NSDate *purchaseDate = parseDate(downloadInfo[@"purchaseDate"] ?: itunesPlist[@"purchaseDate"]); + NSDate *releaseDate = parseDate(downloadInfo[@"releaseDate"] ?: itunesPlist[@"releaseDate"]); + // AppleId & purchaser name + NSString *appleId = accountInfo[@"AppleID"] ?: itunesPlist[@"appleId"]; + NSString *firstName = accountInfo[@"FirstName"]; + NSString *lastName = accountInfo[@"LastName"]; + NSString *name; + if (firstName || lastName) { + name = [NSString stringWithFormat:@"%@ %@ (%@)", firstName, lastName, appleId]; + } else { + name = appleId; + } + + return @{ + @"iTunesHidden": @"", + @"iTunesId": [itunesPlist[@"itemId"] description] ?: @"", + @"iTunesName": itunesPlist[@"itemName"] ?: @"", + @"iTunesGenres": formattedGenres(itunesPlist), + @"iTunesReleaseDate": releaseDate ? formattedDate(releaseDate) : @"", + + @"iTunesAppleId": name ?: @"", + @"iTunesPurchaseDate": purchaseDate ? formattedDate(purchaseDate) : @"", + @"iTunesPrice": itunesPlist[@"priceDisplay"] ?: @"", + }; +} + + +// MARK: - Certificates + +/// Process a single certificate. Extract invalidity / expiration date. +/// @param subject just used for printing error logs. +NSDate * _Nullable getCertificateInvalidityDate(SecCertificateRef certificateRef, NSString *subject) { + NSDate *invalidityDate = nil; + CFErrorRef error = nil; + CFDictionaryRef outerDictRef = SecCertificateCopyValues(certificateRef, (__bridge CFArrayRef)@[(__bridge NSString*)kSecOIDInvalidityDate], &error); + if (outerDictRef) { + CFDictionaryRef innerDictRef = CFDictionaryGetValue(outerDictRef, kSecOIDInvalidityDate); + if (innerDictRef) { + // 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 sure, we'll check: + id value = CFBridgingRelease(CFDictionaryGetValue(innerDictRef, kSecPropertyKeyValue)); + if (value) { + invalidityDate = parseDate(value); + } else { + NSLog(@"No invalidity date in '%@' certificate, dictionary = %@", subject, innerDictRef); + } + // no CFRelease(innerDictRef); since it has the same references as outerDictRef } else { - [output appendFormat:@"%*s%zd bytes of data\n", indent, "", [data length]]; + NSLog(@"No invalidity values in '%@' certificate, dictionary = %@", subject, outerDictRef); } + CFRelease(outerDictRef); } else { - if (key) { - [output appendFormat:@"%*s%@ = %@\n", indent, "", key, value]; + NSLog(@"Could not get values in '%@' certificate, error = %@", subject, error); + CFRelease(error); + } + return invalidityDate; +} + +/// Process list of all certificates. Return a two column table with subject and expiration date. +NSArray * _Nonnull getCertificateList(NSDictionary *provisionPlist) { + NSArray *certArr = provisionPlist[@"DeveloperCertificates"]; + if (![certArr isKindOfClass:[NSArray class]]) { + return @[]; + } + + NSMutableArray *entries = [NSMutableArray array]; + for (NSData *data in certArr) { + SecCertificateRef certificateRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)data); + if (!certificateRef) { + continue; + } + NSString *subject = (NSString *)CFBridgingRelease(SecCertificateCopySubjectSummary(certificateRef)); + if (subject) { + NSDate *invalidityDate = getCertificateInvalidityDate(certificateRef, subject); + NSString *expiration = relativeExpirationDateString(invalidityDate); + [entries addObject:@[subject, expiration ?: @"No invalidity date in certificate"]]; } else { - [output appendFormat:@"%*s%@\n", indent, "", value]; + NSLog(@"Could not get subject from certificate"); } + CFRelease(certificateRef); } + + [entries sortUsingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) { + return [obj1[0] compare:obj2[0]]; + }]; + return entries; } -NSString *expirationStringForDateInCalendar(NSDate *date, NSCalendar *calendar) { - NSString *result = nil; - - if (date) { - NSDateComponentsFormatter *formatter = [[NSDateComponentsFormatter alloc] init]; - formatter.unitsStyle = NSDateComponentsFormatterUnitsStyleFull; - formatter.maximumUnitCount = 1; - - NSDateComponents *dateComponents = [calendar components:(NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute) - fromDate:[NSDate date] - toDate:date - options:0]; - if ([date compare:[NSDate date]] == NSOrderedAscending) { - if ([calendar isDate:date inSameDayAsDate:[NSDate date]]) { - result = @"Expired today"; - } else { - NSDateComponents *reverseDateComponents = [calendar components:(NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute) - fromDate:date - toDate:[NSDate date] - options:0]; - result = [NSString stringWithFormat:@"Expired %@ ago", [formatter stringFromDateComponents:reverseDateComponents]]; - } - } else { - if (dateComponents.day == 0) { - result = @"Expires today"; - } else if (dateComponents.day < 30) { - result = [NSString stringWithFormat:@"Expires in %@", [formatter stringFromDateComponents:dateComponents]]; - } else { - result = [NSString stringWithFormat:@"Expires in %@", [formatter stringFromDateComponents:dateComponents]]; - } - } - - } - - return result; + +// MARK: - Provisioning + +/// Returns provision type string like "Development" or "Distribution (App Store)". +NSString * _Nonnull stringForProfileType(NSDictionary *provisionPlist, BOOL isOSX) { + BOOL hasDevices = [provisionPlist[@"ProvisionedDevices"] isKindOfClass:[NSArray class]]; + if (isOSX) { + return hasDevices ? @"Development" : @"Distribution (App Store)"; + } + if (hasDevices) { + BOOL getTaskAllow = [[provisionPlist[@"Entitlements"] valueForKey:@"get-task-allow"] boolValue]; + return getTaskAllow ? @"Development" : @"Distribution (Ad Hoc)"; + } + BOOL isEnterprise = [provisionPlist[@"ProvisionsAllDevices"] boolValue]; + return isEnterprise ? @"Enterprise" : @"Distribution (App Store)"; } -NSString *formattedStringForCertificates(NSArray *value) { - static NSString *const devCertSummaryKey = @"summary"; - static NSString *const devCertInvalidityDateKey = @"invalidity"; - - NSMutableArray *certificateDetails = [NSMutableArray array]; - NSArray *array = (NSArray *)value; - for (NSData *data in array) { - SecCertificateRef certificateRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)data); - if (certificateRef) { - CFStringRef summaryRef = SecCertificateCopySubjectSummary(certificateRef); - NSString *summary = (NSString *)CFBridgingRelease(summaryRef); - if (summary) { - NSMutableDictionary *detailsDict = [NSMutableDictionary dictionaryWithObject:summary forKey:devCertSummaryKey]; - - CFErrorRef error; - CFDictionaryRef valuesDict = SecCertificateCopyValues(certificateRef, (__bridge CFArrayRef)@[(__bridge id)kSecOIDInvalidityDate], &error); - if (valuesDict) { - CFDictionaryRef invalidityDateDictionaryRef = CFDictionaryGetValue(valuesDict, kSecOIDInvalidityDate); - if (invalidityDateDictionaryRef) { - CFTypeRef invalidityRef = CFDictionaryGetValue(invalidityDateDictionaryRef, kSecPropertyKeyValue); - CFRetain(invalidityRef); - - // 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 sure, we'll check: - id invalidity = CFBridgingRelease(invalidityRef); - if (invalidity) { - if ([invalidity isKindOfClass:[NSDate class]]) { - // use the date directly - [detailsDict setObject:invalidity forKey:devCertInvalidityDateKey]; - } else { - // parse the date from a string - NSString *string = [invalidity description]; - NSDateFormatter *invalidityDateFormatter = [NSDateFormatter new]; - [invalidityDateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"]; - NSDate *invalidityDate = [invalidityDateFormatter dateFromString:string]; - if (invalidityDate) { - [detailsDict setObject:invalidityDate forKey:devCertInvalidityDateKey]; - } - } - } else { - NSLog(@"No invalidity date in '%@' certificate, dictionary = %@", summary, invalidityDateDictionaryRef); - } - } else { - NSLog(@"No invalidity values in '%@' certificate, dictionary = %@", summary, valuesDict); - } - - CFRelease(valuesDict); - } else { - NSLog(@"Could not get values in '%@' certificate, error = %@", summary, error); - } - - [certificateDetails addObject:detailsDict]; - } else { - NSLog(@"Could not get summary from certificate"); - } - - CFRelease(certificateRef); - } - } - - NSMutableString *certificates = [NSMutableString string]; - [certificates appendString:@"\n"]; - - NSArray *sortedCertificateDetails = [certificateDetails sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { - return [((NSDictionary *)obj1)[devCertSummaryKey] compare:((NSDictionary *)obj2)[devCertSummaryKey]]; - }]; - - for (NSDictionary *detailsDict in sortedCertificateDetails) { - NSString *summary = detailsDict[devCertSummaryKey]; - NSDate *invalidityDate = detailsDict[devCertInvalidityDateKey]; - NSString *expiration = expirationStringForDateInCalendar(invalidityDate, [NSCalendar currentCalendar]); - if (! expiration) { - expiration = @"No invalidity date in certificate"; - } - [certificates appendFormat:@"\n", summary, expiration]; - } - [certificates appendString:@"
%@%@
\n"]; - - return [certificates copy]; +/// Enumerate all entries from provison plist with key @c ProvisionedDevices +NSArray * _Nonnull getDeviceList(NSDictionary *provisionPlist) { + NSArray *devArr = provisionPlist[@"ProvisionedDevices"]; + if (![devArr isKindOfClass:[NSArray class]]) { + return @[]; + } + + NSMutableArray *devices = [NSMutableArray array]; + NSString *currentPrefix = nil; + + for (NSString *device in [devArr sortedArrayUsingSelector:@selector(compare:)]) { + // compute the prefix for the first column of the table + NSString *displayPrefix = @""; + NSString *devicePrefix = [device substringToIndex:1]; + if (! [currentPrefix isEqualToString:devicePrefix]) { + currentPrefix = devicePrefix; + displayPrefix = [NSString stringWithFormat:@"%@ âžž ", devicePrefix]; + } + [devices addObject:@[displayPrefix, device]]; + } + return devices; +} + +/// Process info stored in @c embedded.mobileprovision +NSDictionary * _Nonnull procProvision(NSDictionary *provisionPlist, BOOL isOSX) { + if (!provisionPlist) { + return @{ + @"ProvisionHidden": @"hiddenDiv", + }; + } + + NSDate *creationDate = dateOrNil(provisionPlist[@"CreationDate"]); + NSDate *expireDate = dateOrNil(provisionPlist[@"ExpirationDate"]); + NSArray* devices = getDeviceList(provisionPlist); + + return @{ + @"ProvisionHidden": @"", + @"ProfileName": provisionPlist[@"Name"] ?: @"", + @"ProfileUUID": provisionPlist[@"UUID"] ?: @"", + @"TeamName": provisionPlist[@"TeamName"] ?: @"Team name not available", + @"TeamIds": [provisionPlist[@"TeamIdentifier"] componentsJoinedByString:@", "] ?: @"Team ID not available", + @"CreationDateFormatted": creationDate ? formattedCreationDate(creationDate) : @"", + @"ExpirationDateFormatted": expireDate ? formattedExpirationDate(expireDate) : @"", + @"ExpStatus": classNameForExpirationStatus(expireDate), + + @"ProfilePlatform": isOSX ? @"Mac" : @"iOS", + @"ProfileType": stringForProfileType(provisionPlist, isOSX), + + @"ProvisionedDevicesCount": devices.count ? [NSString stringWithFormat:@"%zd Device%s", devices.count, (devices.count == 1 ? "" : "s")] : @"No Devices", + @"ProvisionedDevicesFormatted": devices.count ? formatAsTable(@[@"", @"UDID"], devices) : @"Distribution Profile", + + @"DeveloperCertificatesFormatted": formatAsTable(nil, getCertificateList(provisionPlist)) ?: @"No Developer Certificates", + }; } -NSDictionary *formattedDevicesData(NSArray *value) { - NSArray *array = (NSArray *)value; - NSArray *sortedArray = [array sortedArrayUsingSelector:@selector(compare:)]; +// MARK: - Entitlements - NSString *currentPrefix = nil; - NSMutableString *devices = [NSMutableString string]; - [devices appendString:@"\n"]; - [devices appendString:@"\n"]; +/// Search for app binary and run @c codesign on it. +Entitlements *readEntitlements(QuickLookInfo meta, NSString *bundleExecutable) { + if (!bundleExecutable) { + return [Entitlements withoutBinary]; + } + NSString *tempDirFolder = [NSTemporaryDirectory() stringByAppendingPathComponent:kPluginBundleId]; + NSString *currentTempDirFolder = nil; + NSString *basePath = nil; + switch (meta.type) { + case FileTypeIPA: + currentTempDirFolder = [tempDirFolder stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; + [[NSFileManager defaultManager] createDirectoryAtPath:currentTempDirFolder withIntermediateDirectories:YES attributes:nil error:nil]; + [meta.zipFile unzipFile:[@"Payload/*.app/" stringByAppendingPathComponent:bundleExecutable] toDir:currentTempDirFolder]; + basePath = currentTempDirFolder; + break; + case FileTypeArchive: + basePath = meta.effectiveUrl.path; + break; + case FileTypeExtension: + basePath = meta.url.path; + break; + case FileTypeProvision: + return nil; + } - for (NSString *device in sortedArray) { - // compute the prefix for the first column of the table - NSString *displayPrefix = @""; - NSString *devicePrefix = [device substringToIndex:1]; - if (! [currentPrefix isEqualToString:devicePrefix]) { - currentPrefix = devicePrefix; - displayPrefix = [NSString stringWithFormat:@"%@ âžž ", devicePrefix]; - } + Entitlements *rv = [Entitlements withBinary:[basePath stringByAppendingPathComponent:bundleExecutable]]; + if (currentTempDirFolder) { + [[NSFileManager defaultManager] removeItemAtPath:currentTempDirFolder error:nil]; + } + return rv; +} - [devices appendFormat:@"\n", displayPrefix, device]; - } - [devices appendString:@"
UDID
%@%@
\n"]; +/// Process compiled binary and provision plist to extract @c Entitlements +NSDictionary * _Nonnull procEntitlements(QuickLookInfo meta, NSDictionary *appPlist, NSDictionary *provisionPlist) { + Entitlements *entitlements = readEntitlements(meta, appPlist[@"CFBundleExecutable"]); + [entitlements applyFallbackIfNeeded:provisionPlist[@"Entitlements"]]; - return @{@"ProvisionedDevicesFormatted" : [devices copy], @"ProvisionedDevicesCount" : [NSString stringWithFormat:@"%zd Device%s", [array count], ([array count] == 1 ? "" : "s")]}; + return @{ + @"EntitlementsWarningHidden": entitlements.hasError ? @"" : @"hiddenDiv", + @"EntitlementsFormatted": entitlements.html ?: @"No Entitlements", + }; } -NSString *formattedDictionaryWithReplacements(NSDictionary *dictionary, NSDictionary *replacements, int level) { - - NSMutableString *string = [NSMutableString string]; - - for (NSString *key in dictionary) { - NSString *localizedKey = replacements[key] ?: key; - NSObject *object = dictionary[key]; - - for (int idx = 0; idx < level; idx++) { - if (level == 1) { - [string appendString:@"- "]; - } else { - [string appendString:@"  "]; - } - } - - if ([object isKindOfClass:[NSDictionary class]]) { - object = formattedDictionaryWithReplacements((NSDictionary *)object, replacements, level + 1); - [string appendFormat:@"%@:
%@
", localizedKey, object]; - } - else if ([object isKindOfClass:[NSNumber class]]) { - object = [(NSNumber *)object boolValue] ? @"YES" : @"NO"; - [string appendFormat:@"%@: %@
", localizedKey, object]; - } - else { - [string appendFormat:@"%@: %@
", localizedKey, object]; - } - } - - return string; + +// MARK: - File Info + +/// Title of the preview window +NSString * _Nullable stringForFileType(QuickLookInfo meta) { + switch (meta.type) { + case FileTypeIPA: return @"App info"; + case FileTypeArchive: return @"Archive info"; + case FileTypeExtension: return @"App extension info"; + case FileTypeProvision: return nil; + } + return nil; } -NSString *escapedXML(NSString *stringToEscape) { - stringToEscape = [stringToEscape stringByReplacingOccurrencesOfString:@"&" withString:@"&"]; - NSDictionary *htmlEntityReplacement = @{ - @"\"": @""", - @"'": @"'", - @"<": @"<", - @">": @">", - }; - for (NSString *key in [htmlEntityReplacement allKeys]) { - NSString *replacement = [htmlEntityReplacement objectForKey:key]; - stringToEscape = [stringToEscape stringByReplacingOccurrencesOfString:key withString:replacement]; - } - return stringToEscape; +/// Calculate file / folder size. +unsigned long long getFileSize(NSString *path) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL isDir; + [fileManager fileExistsAtPath:path isDirectory:&isDir]; + if (!isDir) { + return [[fileManager attributesOfItemAtPath:path error:NULL] fileSize]; + } + + unsigned long long fileSize = 0; + NSArray *children = [fileManager subpathsOfDirectoryAtPath:path error:nil]; + for (NSString *fileName in children) { + fileSize += [[fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL] fileSize]; + } + return fileSize; } -NSData *codesignEntitlementsDataFromApp(NSData *infoPlistData, NSString *basePath) { - // read the CFBundleExecutable and extract it - NSDictionary *appPropertyList = [NSPropertyListSerialization propertyListWithData:infoPlistData options:0 format:NULL error:NULL]; - NSString *bundleExecutable = [appPropertyList objectForKey:@"CFBundleExecutable"]; - - NSString *binaryPath = [basePath stringByAppendingPathComponent:bundleExecutable]; - // get entitlements: codesign -d --entitlements - --xml - NSTask *codesignTask = [NSTask new]; - [codesignTask setLaunchPath:@"/usr/bin/codesign"]; - [codesignTask setStandardOutput:[NSPipe pipe]]; - [codesignTask setStandardError:[NSPipe pipe]]; - [codesignTask setArguments:@[@"-d", binaryPath, @"--entitlements", @"-", @"--xml"]]; - [codesignTask launch]; - - NSData *outputData = [[[codesignTask standardOutput] fileHandleForReading] readDataToEndOfFile]; - NSData *errorData = [[[codesignTask standardError] fileHandleForReading] readDataToEndOfFile]; - [codesignTask waitUntilExit]; - - if (outputData.length == 0) { - return errorData; - } - - return outputData; +/// Process meta information about the file itself. Like file size and last modification. +NSDictionary * _Nonnull procFileInfo(NSURL *url) { + NSString *formattedValue = nil; + NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:url.path error:NULL]; + if (attrs) { + formattedValue = [NSString stringWithFormat:@"%@, Modified %@", + [NSByteCountFormatter stringFromByteCount:getFileSize(url.path) countStyle:NSByteCountFormatterCountStyleFile], + formattedDate([attrs fileModificationDate])]; + } + + return @{ + @"FileName": escapedXML([url lastPathComponent]), + @"FileInfo": formattedValue ?: @"", + }; } -OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options) { - @autoreleasepool { - // create temp directory - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSString *tempDirFolder = [NSTemporaryDirectory() stringByAppendingPathComponent:kPluginBundleId]; - NSString *currentTempDirFolder = [tempDirFolder stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; - [fileManager createDirectoryAtPath:currentTempDirFolder withIntermediateDirectories:YES attributes:nil error:nil]; - - NSURL *URL = (__bridge NSURL *)url; - NSString *dataType = (__bridge NSString *)contentTypeUTI; - NSData *provisionData = nil; - NSData *appPlist = nil; - NSData *codesignEntitlementsData = nil; - NSImage *appIcon = nil; - - if ([dataType isEqualToString:kDataType_ipa]) { - // get the embedded provisioning & plist from an app archive using: unzip -u -j -d - NSTask *unzipTask = [NSTask new]; - [unzipTask setLaunchPath:@"/usr/bin/unzip"]; - [unzipTask setArguments:@[@"-u", @"-j", @"-d", currentTempDirFolder, [URL path], @"Payload/*.app/embedded.mobileprovision", @"Payload/*.app/Info.plist", @"-x", @"*/*/*/*"]]; - [unzipTask launch]; - [unzipTask waitUntilExit]; - - NSString *provisionPath = [currentTempDirFolder stringByAppendingPathComponent:@"embedded.mobileprovision"]; - provisionData = [NSData dataWithContentsOfFile:provisionPath]; - NSString *plistPath = [currentTempDirFolder stringByAppendingPathComponent:@"Info.plist"]; - appPlist = [NSData dataWithContentsOfFile:plistPath]; - - // read codesigning entitlements from application binary (extract it first) - NSDictionary *appPropertyList = [NSPropertyListSerialization propertyListWithData:appPlist options:0 format:NULL error:NULL]; - NSString *bundleExecutable = [appPropertyList objectForKey:@"CFBundleExecutable"]; - - NSTask *unzipAppTask = [NSTask new]; - [unzipAppTask setLaunchPath:@"/usr/bin/unzip"]; - [unzipAppTask setArguments:@[@"-u", @"-j", @"-d", currentTempDirFolder, [URL path], [@"Payload/*.app/" stringByAppendingPathComponent:bundleExecutable], @"-x", @"*/*/*/*"]]; - [unzipAppTask launch]; - [unzipAppTask waitUntilExit]; - - codesignEntitlementsData = codesignEntitlementsDataFromApp(appPlist, currentTempDirFolder); - - [fileManager removeItemAtPath:tempDirFolder error:nil]; - } else if ([dataType isEqualToString:kDataType_xcode_archive]) { - // get the embedded plist for the iOS app - NSURL *appsDir = [URL URLByAppendingPathComponent:@"Products/Applications/"]; - if (appsDir != nil) { - NSArray *dirFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:appsDir.path error:nil]; - if (dirFiles.count > 0) { - NSURL *appURL = [appsDir URLByAppendingPathComponent:dirFiles[0] isDirectory:YES]; - - provisionData = [NSData dataWithContentsOfURL:[appURL URLByAppendingPathComponent:@"embedded.mobileprovision"]]; - appPlist = [NSData dataWithContentsOfURL:[appURL URLByAppendingPathComponent:@"Info.plist"]]; - - // read codesigning entitlements from application binary - codesignEntitlementsData = codesignEntitlementsDataFromApp(appPlist, appURL.path); - } - } - } else if ([dataType isEqualToString:kDataType_app_extension]) { - // get embedded plist and provisioning - provisionData = [NSData dataWithContentsOfURL:[URL URLByAppendingPathComponent:@"embedded.mobileprovision"]]; - appPlist = [NSData dataWithContentsOfURL:[URL URLByAppendingPathComponent:@"Info.plist"]]; - // read codesigning entitlements from application binary - codesignEntitlementsData = codesignEntitlementsDataFromApp(appPlist, URL.path); - } else { - // use provisioning directly - provisionData = [NSData dataWithContentsOfURL:URL]; - } - - NSMutableDictionary *synthesizedInfo = [NSMutableDictionary dictionary]; - NSURL *htmlURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"template" withExtension:@"html"]; - NSMutableString *html = [NSMutableString stringWithContentsOfURL:htmlURL encoding:NSUTF8StringEncoding error:NULL]; - NSDateFormatter *dateFormatter = [NSDateFormatter new]; - [dateFormatter setDateStyle:NSDateFormatterMediumStyle]; - [dateFormatter setTimeStyle:NSDateFormatterMediumStyle]; - NSCalendar *calendar = [NSCalendar currentCalendar]; - id value = nil; - NSString *synthesizedValue = nil; - - if ([dataType isEqualToString:kDataType_ipa]) { - [synthesizedInfo setObject:@"App info" forKey:@"AppInfoTitle"]; - } else if ([dataType isEqualToString:kDataType_app_extension]) { - [synthesizedInfo setObject:@"App extension info" forKey:@"AppInfoTitle"]; - } else if ([dataType isEqualToString:kDataType_xcode_archive]) { - [synthesizedInfo setObject:@"Archive info" forKey:@"AppInfoTitle"]; - } - - if (!provisionData) { - NSLog(@"No provisionData for %@", URL); - - if (appPlist != nil) { - [synthesizedInfo setObject:@"hiddenDiv" forKey:@"ProvisionInfo"]; - } else { - return noErr; - } - } else { - [synthesizedInfo setObject:@"" forKey:@"ProvisionInfo"]; - } - - // MARK: App Info - - if (appPlist != nil) { - NSDictionary *appPropertyList = [NSPropertyListSerialization propertyListWithData:appPlist options:0 format:NULL error:NULL]; - - NSString *bundleName = [appPropertyList objectForKey:@"CFBundleDisplayName"]; - if (!bundleName) { - bundleName = [appPropertyList objectForKey:@"CFBundleName"]; - } - [synthesizedInfo setObject:bundleName forKey:@"CFBundleName"]; - [synthesizedInfo setObject:[appPropertyList objectForKey:@"CFBundleIdentifier"] forKey:@"CFBundleIdentifier"]; - [synthesizedInfo setObject:[appPropertyList objectForKey:@"CFBundleShortVersionString"] forKey:@"CFBundleShortVersionString"]; - [synthesizedInfo setObject:[appPropertyList objectForKey:@"CFBundleVersion"] forKey:@"CFBundleVersion"]; - - NSString *extensionType = [[appPropertyList objectForKey:@"NSExtension"] objectForKey:@"NSExtensionPointIdentifier"]; - if(extensionType != nil) { - [synthesizedInfo setObject:@"" forKey:@"ExtensionInfo"]; - [synthesizedInfo setObject:extensionType forKey:@"NSExtensionPointIdentifier"]; - } else { - [synthesizedInfo setObject:@"hiddenDiv" forKey:@"ExtensionInfo"]; - } - - NSString *sdkName = [appPropertyList objectForKey:@"DTSDKName"] ?: @""; - [synthesizedInfo setObject:sdkName forKey:@"DTSDKName"]; - - NSString *minimumOSVersion = [appPropertyList objectForKey:@"MinimumOSVersion"] ?: @""; - [synthesizedInfo setObject:minimumOSVersion forKey:@"MinimumOSVersion"]; - - NSDictionary *appTransportSecurity = [appPropertyList objectForKey:@"NSAppTransportSecurity"]; - NSString *appTransportSecurityFormatted = @"No exceptions"; - if ([appTransportSecurity isKindOfClass:[NSDictionary class]]) { - NSDictionary *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" - }; - - NSString *formattedDictionaryString = formattedDictionaryWithReplacements(appTransportSecurity, localizedKeys, 0); - appTransportSecurityFormatted = [NSString stringWithFormat:@"
%@
", formattedDictionaryString]; - } else { - double sdkNumber = [[sdkName stringByTrimmingCharactersInSet:[NSCharacterSet letterCharacterSet]] doubleValue]; - if (sdkNumber < 9.0) { - appTransportSecurityFormatted = @"Not applicable before iOS 9.0"; - } - } - - [synthesizedInfo setObject:appTransportSecurityFormatted forKey:@"AppTransportSecurityFormatted"]; - - NSString *iconName = mainIconNameForApp(appPropertyList); - appIcon = imageFromApp(URL, dataType, iconName); - - NSMutableArray *platforms = [NSMutableArray array]; - for (NSNumber *number in [appPropertyList objectForKey:@"UIDeviceFamily"]) { - switch ([number intValue]) { - case 1: - [platforms addObject:@"iPhone"]; - break; - case 2: - [platforms addObject:@"iPad"]; - break; - case 3: - [platforms addObject:@"TV"]; - break; - case 4: - [platforms addObject:@"Watch"]; - break; - default: - break; - } - } - [synthesizedInfo setObject:[platforms componentsJoinedByString:@", "] forKey:@"UIDeviceFamily"]; - - if (!appIcon) { - NSURL *iconURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"defaultIcon" withExtension:@"png"]; - appIcon = [[NSImage alloc] initWithContentsOfURL:iconURL]; - } - appIcon = roundCorners(appIcon); - - NSData *imageData = [appIcon TIFFRepresentation]; - NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData]; - imageData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; - NSString *base64 = [imageData base64EncodedStringWithOptions:0]; - [synthesizedInfo setObject:base64 forKey:@"AppIcon"]; - [synthesizedInfo setObject:@"" forKey:@"AppInfo"]; - [synthesizedInfo setObject:@"hiddenDiv" forKey:@"ProvisionAsSubheader"]; - } else { - [synthesizedInfo setObject:@"hiddenDiv" forKey:@"AppInfo"]; - [synthesizedInfo setObject:@"" forKey:@"ProvisionAsSubheader"]; - } - - // MARK: Provisioning - - CMSDecoderRef decoder = NULL; - CMSDecoderCreate(&decoder); - CMSDecoderUpdateMessage(decoder, provisionData.bytes, provisionData.length); - CMSDecoderFinalizeMessage(decoder); - CFDataRef dataRef = NULL; - CMSDecoderCopyContent(decoder, &dataRef); - NSData *data = (NSData *)CFBridgingRelease(dataRef); - CFRelease(decoder); - - if ((!data && !appPlist) || QLPreviewRequestIsCancelled(preview)) { - return noErr; - } - - if (data) { - // use all keys and values in the property list to generate replacement tokens and values - NSDictionary *propertyList = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL]; - for (NSString *key in [propertyList allKeys]) { - NSString *replacementValue = [[propertyList valueForKey:key] description]; - NSString *replacementToken = [NSString stringWithFormat:@"__%@__", key]; - [html replaceOccurrencesOfString:replacementToken withString:replacementValue options:0 range:NSMakeRange(0, [html length])]; - } - - // synthesize other replacement tokens and values - value = [propertyList objectForKey:@"CreationDate"]; - if ([value isKindOfClass:[NSDate class]]) { - NSDate *date = (NSDate *)value; - synthesizedValue = [dateFormatter stringFromDate:date]; - [synthesizedInfo setObject:synthesizedValue forKey:@"CreationDateFormatted"]; - - NSDateComponents *dateComponents = [calendar components:(NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute) - fromDate:date - toDate:[NSDate date] - options:0]; - if ([calendar isDate:date inSameDayAsDate:[NSDate date]]) { - synthesizedValue = @"Created today"; - } else { - NSDateComponentsFormatter *formatter = [[NSDateComponentsFormatter alloc] init]; - formatter.unitsStyle = NSDateComponentsFormatterUnitsStyleFull; - formatter.maximumUnitCount = 1; - - synthesizedValue = [NSString stringWithFormat:@"Created %@ ago", [formatter stringFromDateComponents:dateComponents]]; - } - [synthesizedInfo setObject:synthesizedValue forKey:@"CreationSummary"]; - } - - value = [propertyList objectForKey:@"ExpirationDate"]; - if ([value isKindOfClass:[NSDate class]]) { - NSDate *date = (NSDate *)value; - synthesizedValue = [dateFormatter stringFromDate:date]; - [synthesizedInfo setObject:synthesizedValue forKey:@"ExpirationDateFormatted"]; - - synthesizedValue = expirationStringForDateInCalendar(date, calendar); - [synthesizedInfo setObject:synthesizedValue forKey:@"ExpirationSummary"]; - - int expStatus = expirationStatus(date, calendar); - if (expStatus == 0) { - synthesizedValue = @"expired"; - } else if (expStatus == 1) { - synthesizedValue = @"expiring"; - } else { - synthesizedValue = @"valid"; - } - [synthesizedInfo setObject:synthesizedValue forKey:@"ExpStatus"]; - } - - value = [propertyList objectForKey:@"TeamIdentifier"]; - if ([value isKindOfClass:[NSArray class]]) { - NSArray *array = (NSArray *)value; - synthesizedValue = [array componentsJoinedByString:@", "]; - [synthesizedInfo setObject:synthesizedValue forKey:@"TeamIds"]; - } - - BOOL showEntitlementsWarning = false; - if (codesignEntitlementsData != nil) { - // read the entitlements directly from the codesign output - NSDictionary *entitlementsPropertyList = [NSPropertyListSerialization propertyListWithData:codesignEntitlementsData options:0 format:NULL error:NULL]; - if (entitlementsPropertyList != nil) { - NSMutableString *dictionaryFormatted = [NSMutableString string]; - displayKeyAndValue(0, nil, entitlementsPropertyList, dictionaryFormatted); - synthesizedValue = [NSString stringWithFormat:@"
%@
", dictionaryFormatted]; - } else { - NSString *outputString = [[NSString alloc] initWithData:codesignEntitlementsData encoding:NSUTF8StringEncoding]; - NSString *errorOutput; - if ([outputString hasPrefix:@"Executable="]) { - // remove first line with long temporary path to the executable - NSArray *allLines = [outputString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; - errorOutput = [[allLines subarrayWithRange:NSMakeRange(1, allLines.count - 1)] componentsJoinedByString:@"
"]; - } else { - errorOutput = outputString; - } - showEntitlementsWarning = true; - synthesizedValue = errorOutput; - } - [synthesizedInfo setObject:synthesizedValue forKey:@"EntitlementsFormatted"]; - } else { - // read the entitlements from the provisioning profile instead - value = [propertyList objectForKey:@"Entitlements"]; - if ([value isKindOfClass:[NSDictionary class]]) { - NSDictionary *dictionary = (NSDictionary *)value; - NSMutableString *dictionaryFormatted = [NSMutableString string]; - displayKeyAndValue(0, nil, dictionary, dictionaryFormatted); - synthesizedValue = [NSString stringWithFormat:@"
%@
", dictionaryFormatted]; - - [synthesizedInfo setObject:synthesizedValue forKey:@"EntitlementsFormatted"]; - } else { - [synthesizedInfo setObject:@"No Entitlements" forKey:@"EntitlementsFormatted"]; - } - } - if (showEntitlementsWarning) { - [synthesizedInfo setObject:@"" forKey:@"EntitlementsWarning"]; - } else { - [synthesizedInfo setObject:@"hiddenDiv" forKey:@"EntitlementsWarning"]; - } - - value = [propertyList objectForKey:@"DeveloperCertificates"]; - if ([value isKindOfClass:[NSArray class]]) { - [synthesizedInfo setObject:formattedStringForCertificates(value) forKey:@"DeveloperCertificatesFormatted"]; - } else { - [synthesizedInfo setObject:@"No Developer Certificates" forKey:@"DeveloperCertificatesFormatted"]; - } - - value = [propertyList objectForKey:@"ProvisionedDevices"]; - if ([value isKindOfClass:[NSArray class]]) { - [synthesizedInfo addEntriesFromDictionary:formattedDevicesData(value)]; - } else { - [synthesizedInfo setObject:@"No Devices" forKey:@"ProvisionedDevicesFormatted"]; - [synthesizedInfo setObject:@"Distribution Profile" forKey:@"ProvisionedDevicesCount"]; - } - - { - NSString *profileString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - profileString = escapedXML(profileString); - synthesizedValue = [NSString stringWithFormat:@"
%@
", profileString]; - [synthesizedInfo setObject:synthesizedValue forKey:@"RawData"]; - } - - // older provisioning files don't include some key/value pairs - value = [propertyList objectForKey:@"TeamName"]; - if (! value) { - [synthesizedInfo setObject:@"Team name not available" forKey:@"TeamName"]; - } - value = [propertyList objectForKey:@"TeamIdentifier"]; - if (! value) { - [synthesizedInfo setObject:@"Team ID not available" forKey:@"TeamIds"]; - } - value = [propertyList objectForKey:@"AppIDName"]; - if (! value) { - [synthesizedInfo setObject:@"App name not available" forKey:@"AppIDName"]; - } - - // determine the profile type - BOOL getTaskAllow = NO; - value = [propertyList objectForKey:@"Entitlements"]; - if ([value isKindOfClass:[NSDictionary class]]) { - NSDictionary *dictionary = (NSDictionary *)value; - getTaskAllow = [[dictionary valueForKey:@"get-task-allow"] boolValue]; - } - - BOOL hasDevices = NO; - value = [propertyList objectForKey:@"ProvisionedDevices"]; - if ([value isKindOfClass:[NSArray class]]) { - hasDevices = YES; - } - - BOOL isEnterprise = [[propertyList objectForKey:@"ProvisionsAllDevices"] boolValue]; - - if ([dataType isEqualToString:kDataType_osx_provision]) { - [synthesizedInfo setObject:@"mac" forKey:@"Platform"]; - - [synthesizedInfo setObject:@"Mac" forKey:@"ProfilePlatform"]; - if (hasDevices) { - [synthesizedInfo setObject:@"Development" forKey:@"ProfileType"]; - } else { - [synthesizedInfo setObject:@"Distribution (App Store)" forKey:@"ProfileType"]; - } - } else { - [synthesizedInfo setObject:@"ios" forKey:@"Platform"]; - - [synthesizedInfo setObject:@"iOS" forKey:@"ProfilePlatform"]; - if (hasDevices) { - if (getTaskAllow) { - [synthesizedInfo setObject:@"Development" forKey:@"ProfileType"]; - } else { - [synthesizedInfo setObject:@"Distribution (Ad Hoc)" forKey:@"ProfileType"]; - } - } else { - if (isEnterprise) { - [synthesizedInfo setObject:@"Enterprise" forKey:@"ProfileType"]; - } else { - [synthesizedInfo setObject:@"Distribution (App Store)" forKey:@"ProfileType"]; - } - } - } - } - - // MARK: File Info - - [synthesizedInfo setObject:escapedXML([URL lastPathComponent]) forKey:@"FileName"]; - - if ([[URL pathExtension] isEqualToString:@"app"] || [[URL pathExtension] isEqualToString:@"appex"]) { - // get the "file" information using the application package folder - NSString *folderPath = [URL path]; - - NSDictionary *folderAttributes = [fileManager attributesOfItemAtPath:folderPath error:NULL]; - if (folderAttributes) { - NSDate *folderModificationDate = [folderAttributes fileModificationDate]; - - unsigned long long folderSize = 0; - NSArray *filesArray = [fileManager subpathsOfDirectoryAtPath:folderPath error:nil]; - for (NSString *fileName in filesArray) { - NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:[folderPath stringByAppendingPathComponent:fileName] error:NULL]; - if (fileAttributes) - folderSize += [fileAttributes fileSize]; - } - - synthesizedValue = [NSString stringWithFormat:@"%@, Modified %@", - [NSByteCountFormatter stringFromByteCount:folderSize countStyle:NSByteCountFormatterCountStyleFile], - [dateFormatter stringFromDate:folderModificationDate]]; - [synthesizedInfo setObject:synthesizedValue forKey:@"FileInfo"]; - } else { - [synthesizedInfo setObject:@"" forKey:@"FileInfo"]; - } - } else { - NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:[URL path] error:NULL]; - if (fileAttributes) { - NSDate *fileModificationDate = [fileAttributes fileModificationDate]; - unsigned long long fileSize = [fileAttributes fileSize]; - - synthesizedValue = [NSString stringWithFormat:@"%@, Modified %@", - [NSByteCountFormatter stringFromByteCount:fileSize countStyle:NSByteCountFormatterCountStyleFile], - [dateFormatter stringFromDate:fileModificationDate]]; - [synthesizedInfo setObject:synthesizedValue forKey:@"FileInfo"]; - } - } - - // MARK: Footer +// MARK: - Footer Info + +/// Process meta information about the plugin. Like version and debug flag. +NSDictionary * _Nonnull procFooterInfo() { + NSBundle *mainBundle = [NSBundle bundleWithIdentifier:kPluginBundleId]; + return @{ #ifdef DEBUG - [synthesizedInfo setObject:@"(debug)" forKey:@"DEBUG"]; + @"DEBUG": @"(debug)", #else - [synthesizedInfo setObject:@"" forKey:@"DEBUG"]; + @"DEBUG": @"", #endif + @"BundleShortVersionString": [mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"", + @"BundleVersion": [mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"] ?: @"", + }; +} - synthesizedValue = [[NSBundle bundleWithIdentifier:kPluginBundleId] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; - [synthesizedInfo setObject:synthesizedValue forKey:@"BundleShortVersionString"]; - synthesizedValue = [[NSBundle bundleWithIdentifier:kPluginBundleId] objectForInfoDictionaryKey:@"CFBundleVersion"]; - [synthesizedInfo setObject:synthesizedValue forKey:@"BundleVersion"]; +// MARK: - Main Entry + +NSString *applyHtmlTemplate(NSDictionary *templateValues) { + NSURL *templateURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"template" withExtension:@"html"]; + NSString *html = [NSString stringWithContentsOfURL:templateURL encoding:NSUTF8StringEncoding error:NULL]; + + // this is less efficient +// for (NSString *key in [templateValues allKeys]) { +// [html replaceOccurrencesOfString:[NSString stringWithFormat:@"__%@__", key] +// withString:[templateValues objectForKey:key] +// options:0 range:NSMakeRange(0, [html length])]; +// } + + NSMutableString *rv = [NSMutableString string]; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"__[^ _]{1,40}?__" options:0 error:nil]; + __block NSUInteger prevLoc = 0; + [regex enumerateMatchesInString:html options:0 range:NSMakeRange(0, html.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) { + NSUInteger start = result.range.location; + NSString *key = [html substringWithRange:NSMakeRange(start + 2, result.range.length - 4)]; + [rv appendString:[html substringWithRange:NSMakeRange(prevLoc, start - prevLoc)]]; + NSString *value = templateValues[key]; + if (value) { + [rv appendString:value]; + } + prevLoc = start + result.range.length; + }]; + [rv appendString:[html substringWithRange:NSMakeRange(prevLoc, html.length - prevLoc)]]; + return rv; +} - for (NSString *key in [synthesizedInfo allKeys]) { - NSString *replacementValue = [synthesizedInfo objectForKey:key]; - NSString *replacementToken = [NSString stringWithFormat:@"__%@__", key]; - [html replaceOccurrencesOfString:replacementToken withString:replacementValue options:0 range:NSMakeRange(0, [html length])]; - } +OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options) { + @autoreleasepool { + QuickLookInfo meta = initQLInfo(contentTypeUTI, url); + if (!meta.type) { + return noErr; + } + NSMutableDictionary* infoLayer = [NSMutableDictionary dictionary]; + infoLayer[@"AppInfoTitle"] = stringForFileType(meta); - NSDictionary *properties = @{ // properties for the HTML data - (__bridge NSString *)kQLPreviewPropertyTextEncodingNameKey : @"UTF-8", - (__bridge NSString *)kQLPreviewPropertyMIMETypeKey : @"text/html" }; + // App Info + NSDictionary *plistApp = readPlistApp(meta); + [infoLayer addEntriesFromDictionary:procAppInfo(plistApp)]; + ALLOW_EXIT - QLPreviewRequestSetDataRepresentation(preview, (__bridge CFDataRef)[html dataUsingEncoding:NSUTF8StringEncoding], kUTTypeHTML, (__bridge CFDictionaryRef)properties); - } + NSDictionary *plistItunes = readPlistItunes(meta); + [infoLayer addEntriesFromDictionary:parseItunesMeta(plistItunes)]; + ALLOW_EXIT + + // Provisioning + NSDictionary *plistProvision = readPlistProvision(meta); + + if (!plistApp && !plistProvision) { + return noErr; // nothing to do. Maybe another QL plugin can do better. + } + [infoLayer addEntriesFromDictionary:procProvision(plistProvision, meta.isOSX)]; + ALLOW_EXIT + + // Entitlements + [infoLayer addEntriesFromDictionary:procEntitlements(meta, plistApp, plistProvision)]; + ALLOW_EXIT + + // File Info + [infoLayer addEntriesFromDictionary:procFileInfo(meta.url)]; + + // Footer Info + [infoLayer addEntriesFromDictionary:procFooterInfo()]; + ALLOW_EXIT + + // App Icon (last, because the image uses a lot of memory) + AppIcon *icon = [AppIcon load:meta]; + if (icon.canExtractImage) { + infoLayer[@"AppIcon"] = [[[icon extractImage:plistApp] withRoundCorners] asBase64]; + ALLOW_EXIT + } + + // prepare html, replace values + NSString *html = applyHtmlTemplate(infoLayer); + + // QL render html + NSDictionary *properties = @{ // properties for the HTML data + (__bridge NSString *)kQLPreviewPropertyTextEncodingNameKey : @"UTF-8", + (__bridge NSString *)kQLPreviewPropertyMIMETypeKey : @"text/html" + }; + QLPreviewRequestSetDataRepresentation(preview, (__bridge CFDataRef)[html dataUsingEncoding:NSUTF8StringEncoding], kUTTypeHTML, (__bridge CFDictionaryRef)properties); + } return noErr; } -void CancelThumbnailGeneration(void *thisInterface, QLThumbnailRequestRef thumbnail) { - // Implement only if supported +void CancelPreviewGeneration(void *thisInterface, QLPreviewRequestRef preview) { + // Implement only if supported } diff --git a/ProvisionQL/GenerateThumbnailForURL.m b/ProvisionQL/GenerateThumbnailForURL.m index f1334c2..a94be2e 100644 --- a/ProvisionQL/GenerateThumbnailForURL.m +++ b/ProvisionQL/GenerateThumbnailForURL.m @@ -1,4 +1,8 @@ #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 @@ -19,173 +23,138 @@ void CancelThumbnailGeneration(void *thisInterface, QLThumbnailRequestRef thumbnail); /* ----------------------------------------------------------------------------- - Generate a thumbnail for file + 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; +} + - This function's job is to create thumbnail for designated file as fast as possible - ----------------------------------------------------------------------------- */ +// MARK: Main Entry OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thumbnail, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options, CGSize maxSize) { - @autoreleasepool { - NSURL *URL = (__bridge NSURL *)url; - NSString *dataType = (__bridge NSString *)contentTypeUTI; - NSDictionary *optionsDict = (__bridge NSDictionary *)options; - BOOL iconMode = ([optionsDict objectForKey:(NSString *)kQLThumbnailOptionIconModeKey]) ? YES : NO; - NSData *provisionData = nil; - NSData *appPlist = nil; - NSImage *appIcon = nil; - NSUInteger devicesCount = 0; - int expStatus = 0; - - if ([dataType isEqualToString:kDataType_xcode_archive]) { - // get the embedded plist for the iOS app - NSURL *appsDir = [URL URLByAppendingPathComponent:@"Products/Applications/"]; - if (appsDir != nil) { - NSArray *dirFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:appsDir.path error:nil]; - if (dirFiles.count > 0) { - appPlist = [NSData dataWithContentsOfURL:[appsDir URLByAppendingPathComponent:[NSString stringWithFormat:@"%@/Info.plist", dirFiles[0]]]]; - } - } - } else if([dataType isEqualToString:kDataType_ipa]) { - // get the embedded plist from an app archive using: unzip -p 'Payload/*.app/Info.plist' (piped to standard output) - NSTask *unzipTask = [NSTask new]; - [unzipTask setLaunchPath:@"/usr/bin/unzip"]; - [unzipTask setStandardOutput:[NSPipe pipe]]; - [unzipTask setArguments:@[@"-p", [URL path], @"Payload/*.app/Info.plist", @"-x", @"*/*/*/*"]]; - [unzipTask launch]; - - NSData *pipeData = [[[unzipTask standardOutput] fileHandleForReading] readDataToEndOfFile]; - [unzipTask waitUntilExit]; - - appPlist = pipeData; - } else { - // use provisioning directly - provisionData = [NSData dataWithContentsOfURL:URL]; - } - - if (QLThumbnailRequestIsCancelled(thumbnail)) { - return noErr; - } - - NSDictionary *propertiesDict = nil; - if ([dataType isEqualToString:kDataType_ipa] || [dataType isEqualToString:kDataType_xcode_archive]) { - NSDictionary *appPropertyList = [NSPropertyListSerialization propertyListWithData:appPlist options:0 format:NULL error:NULL]; - NSString *iconName = mainIconNameForApp(appPropertyList); - appIcon = imageFromApp(URL, dataType, iconName); - - if (!appIcon) { - NSURL *iconURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"defaultIcon" withExtension:@"png"]; - appIcon = [[NSImage alloc] initWithContentsOfURL:iconURL]; - } - appIcon = roundCorners(appIcon); - if ([dataType isEqualToString:kDataType_xcode_archive]) { - propertiesDict = @{@"IconFlavor" : @(12)}; - } else { - propertiesDict = @{@"IconFlavor" : @(0)}; - } - } else { - if (iconMode) { - NSURL *iconURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"blankIcon" withExtension:@"png"]; - appIcon = [[NSImage alloc] initWithContentsOfURL:iconURL]; - } else { - appIcon = [[NSWorkspace sharedWorkspace] iconForFileType:dataType]; - [appIcon setSize:NSMakeSize(512,512)]; - } - - if (!provisionData) { - NSLog(@"No provisionData for %@", URL); - return noErr; - } - - CMSDecoderRef decoder = NULL; - CMSDecoderCreate(&decoder); - CMSDecoderUpdateMessage(decoder, provisionData.bytes, provisionData.length); - CMSDecoderFinalizeMessage(decoder); - CFDataRef dataRef = NULL; - CMSDecoderCopyContent(decoder, &dataRef); - NSData *data = (NSData *)CFBridgingRelease(dataRef); - CFRelease(decoder); - - if (!data || QLThumbnailRequestIsCancelled(thumbnail)) { - return noErr; - } - - NSDictionary *propertyList = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL]; - id value = [propertyList objectForKey:@"ProvisionedDevices"]; - if ([value isKindOfClass:[NSArray class]]) { - devicesCount = [value count]; - } - - value = [propertyList objectForKey:@"ExpirationDate"]; - if ([value isKindOfClass:[NSDate class]]) { - expStatus = expirationStatus(value, [NSCalendar currentCalendar]); - } - } - - if (QLThumbnailRequestIsCancelled(thumbnail)) { - return noErr; - } - - NSSize canvasSize = appIcon.size; - NSRect renderRect = NSMakeRect(0.0, 0.0, appIcon.size.width, appIcon.size.height); - - CGContextRef _context = QLThumbnailRequestCreateContext(thumbnail, canvasSize, false, (__bridge CFDictionaryRef)propertiesDict); - if (_context) { - NSGraphicsContext *_graphicsContext = [NSGraphicsContext graphicsContextWithCGContext:(void *)_context flipped:NO]; - - [NSGraphicsContext setCurrentContext:_graphicsContext]; - if ([dataType isEqualToString:kDataType_ipa] || [dataType isEqualToString:kDataType_xcode_archive]) { - [appIcon drawInRect:renderRect]; - } else { - [appIcon drawInRect:renderRect]; - - NSString *badge = [NSString stringWithFormat:@"%lu",(unsigned long)devicesCount]; - NSColor *outlineColor; - - if (expStatus == 2) { - outlineColor = BADGE_VALID_COLOR; - } else if (expStatus == 1) { - outlineColor = BADGE_EXPIRING_COLOR; - } else { - outlineColor = BADGE_EXPIRED_COLOR; - } - - NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail; - paragraphStyle.alignment = NSTextAlignmentCenter; - - NSDictionary *attrDict = @{NSFontAttributeName : BADGE_FONT, NSForegroundColorAttributeName : outlineColor, NSParagraphStyleAttributeName: paragraphStyle}; - - NSSize badgeNumSize = [badge sizeWithAttributes:attrDict]; - int badgeWidth = badgeNumSize.width + BADGE_MARGIN * 2; - badgeWidth = MAX(badgeWidth, MIN_BADGE_WIDTH); - - 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; - NSRect badgeRect = NSMakeRect(badgeX, badgeY, badgeWidth, BADGE_HEIGHT); - - NSBezierPath *badgePath = [NSBezierPath bezierPathWithRoundedRect:badgeRect xRadius:10 yRadius:10]; - [badgePath setLineWidth:8.0]; - [BADGE_BG_COLOR set]; - [badgePath fill]; - [outlineColor set]; - [badgePath stroke]; - - [badge drawAtPoint:NSMakePoint(badgeNumX,badgeY) withAttributes:attrDict]; - } - - QLThumbnailRequestFlushContext(thumbnail, _context); - CFRelease(_context); - } - } - - return noErr; + @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 CancelPreviewGeneration(void *thisInterface, QLPreviewRequestRef preview) { - // Implement only if supported +void CancelThumbnailGeneration(void *thisInterface, QLThumbnailRequestRef thumbnail) { + // Implement only if supported } diff --git a/ProvisionQL/Resources/template.html b/ProvisionQL/Resources/template.html index 6eb172a..8e02dda 100644 --- a/ProvisionQL/Resources/template.html +++ b/ProvisionQL/Resources/template.html @@ -1,196 +1,215 @@ - - - - - - -
-

__AppInfoTitle__

-
App icon
-
+ + + + + + +
+

__AppInfoTitle__

+
App icon
+
Name: __CFBundleName__
Version: __CFBundleShortVersionString__ (__CFBundleVersion__)
BundleId: __CFBundleIdentifier__
-
- Extension type: __NSExtensionPointIdentifier__
-
+
+ Extension type: __ExtensionType__
+
DeviceFamily: __UIDeviceFamily__
- SDK: __DTSDKName__
- Minimum OS Version: __MinimumOSVersion__
-
-
-

App Transport Security

- __AppTransportSecurityFormatted__ -
- -
-
-

Provisioning

- Profile name: __Name__
-
-
-

__Name__

-
- - Profile UUID: __UUID__
+ SDK: __DTSDKName__
+ Minimum OS Version: __MinimumOSVersion__
+
+
+

App Transport Security

+ __AppTransportSecurityFormatted__ +
+ +
+
+

Provisioning

+ Profile name: __ProfileName__
+
+
+

__ProfileName__

+
+ + Profile UUID: __ProfileUUID__
Profile Type: __ProfilePlatform__ __ProfileType__
Team: __TeamName__ (__TeamIds__)
- Creation date: __CreationDateFormatted__ (__CreationSummary__)
- Expiration Date: __ExpirationDateFormatted__ (__ExpirationSummary__)
+ Creation date: __CreationDateFormatted__
+ Expiration Date: __ExpirationDateFormatted__
+
-

Entitlements

-
- Entitlements extraction failed. -
- __EntitlementsFormatted__ +
+

Entitlements

+
+ Entitlements extraction failed. +
+ __EntitlementsFormatted__ +
+

Developer Certificates

__DeveloperCertificatesFormatted__ +
+

Devices (__ProvisionedDevicesCount__)

- __ProvisionedDevicesFormatted__ + __ProvisionedDevicesFormatted__ +
+
+

iTunes Metadata

+ iTunesId: __iTunesId__
+ Title: __iTunesName__
+ Genres: __iTunesGenres__
+ Released: __iTunesReleaseDate__
+
+ AppleId: __iTunesAppleId__
+ Purchased: __iTunesPurchaseDate__
+ Price: __iTunesPrice__
+
+ +
+

File info

+ __FileName__
+ __FileInfo__
+
+ -
-

File info

- __FileName__
- __FileInfo__
-
- - + diff --git a/ProvisionQL/Scripts/test_generate_all.sh b/ProvisionQL/Scripts/test_generate_all.sh new file mode 100755 index 0000000..4e00d23 --- /dev/null +++ b/ProvisionQL/Scripts/test_generate_all.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Usage: this_script ~/Downloads/test_IPAs/*.ipa + +OUT_DIR=gen_output + +ql() { + # QL_dir=$HOME/Library/QuickLook + QL_dir=$(dirname "$HOME/Library/Developer/Xcode/DerivedData/ProvisionQL-"*"/Build/Products/Debug/.") + QL_generator="$QL_dir/ProvisionQL.qlgenerator" + QL_type=$(mdls -raw -name kMDItemContentType "$1") + qlmanage -g "$QL_generator" -c "$QL_type" "$@" 1> /dev/null +} + +thumb() { + echo + echo "=== Thumbnail: $1 ===" + ql "$1" -t -i -s 1024 -o "$OUT_DIR" + bn=$(basename "$1") + mv "$OUT_DIR/$bn.png" "$OUT_DIR/t_$bn.png" +} + +preview() { + echo + echo "=== Preview: $1 ===" + ql "$1" -p -o "$OUT_DIR" + bn=$(basename "$1") + mv "$OUT_DIR/$bn.qlpreview/Preview.html" "$OUT_DIR/p_$bn.html" + rm -rf "$OUT_DIR/$bn.qlpreview" +} + +fn() { + thumb "$1" + preview "$1" +} + + + +mkdir -p "$OUT_DIR" + +for file in "$@"; do + if [ -e "$file" ]; then + fn "$file" + fi +done + +echo 'done.' + +# fn 'a.appex' +# fn 'a.xcarchive' +# fn 'a.mobileprovision' + +# for x in *.ipa; do +# fn "$x" +# done +# fn 'a.ipa' +# fn 'aa.ipa' +# fn 'at.ipa' +# fn '10.Flight.Control-v1.9.ipa' +# fn 'Labyrinth 2 HD.ipa' +# fn 'Plague Inc. 1.10.1.ipa' +# fn 'iTunes U 1.3.1.ipa' diff --git a/ProvisionQL/Shared.h b/ProvisionQL/Shared.h index ca9328c..78b66ef 100644 --- a/ProvisionQL/Shared.h +++ b/ProvisionQL/Shared.h @@ -6,17 +6,52 @@ #import #import -#import - -static NSString * const kPluginBundleId = @"com.ealeksandrov.ProvisionQL"; -static NSString * const kDataType_ipa = @"com.apple.itunes.ipa"; -static NSString * const kDataType_ios_provision = @"com.apple.mobileprovision"; -static NSString * const kDataType_ios_provision_old = @"com.apple.iphone.mobileprovision"; -static NSString * const kDataType_osx_provision = @"com.apple.provisionprofile"; -static NSString * const kDataType_xcode_archive = @"com.apple.xcode.archive"; -static NSString * const kDataType_app_extension = @"com.apple.application-and-system-extension"; - -NSImage *roundCorners(NSImage *image); -NSImage *imageFromApp(NSURL *URL, NSString *dataType, NSString *fileName); -NSString *mainIconNameForApp(NSDictionary *appPropertyList); -int expirationStatus(NSDate *date, NSCalendar *calendar); +#import "ZipFile.h" + +static NSString * _Nonnull const kPluginBundleId = @"com.ealeksandrov.ProvisionQL"; +static NSString * _Nonnull const kDataType_ipa = @"com.apple.itunes.ipa"; +static NSString * _Nonnull const kDataType_ios_provision = @"com.apple.mobileprovision"; +static NSString * _Nonnull const kDataType_ios_provision_old = @"com.apple.iphone.mobileprovision"; +static NSString * _Nonnull const kDataType_osx_provision = @"com.apple.provisionprofile"; +static NSString * _Nonnull const kDataType_xcode_archive = @"com.apple.xcode.archive"; +static NSString * _Nonnull const kDataType_app_extension = @"com.apple.application-and-system-extension"; + +// 3rd party ipa-like file extensions +static NSString * _Nonnull const kDataType_trollstore_ipa = @"com.opa334.trollstore.tipa"; +static NSString * _Nonnull const kDataType_trollstore_ipa_dyn = @"dyn.ah62d4rv4ge81k4puqe"; + +// Init QuickLook Type +typedef NS_ENUM(NSUInteger, FileType) { + FileTypeIPA = 1, + FileTypeArchive, + FileTypeExtension, + FileTypeProvision, +}; + +typedef struct QuickLookMeta { + NSString * _Nonnull UTI; + NSURL * _Nonnull url; + NSURL * _Nullable effectiveUrl; // if set, will point to the app inside of an archive + + FileType type; + BOOL isOSX; + ZipFile * _Nullable zipFile; // only set for zipped file types +} QuickLookInfo; + +QuickLookInfo initQLInfo(_Nonnull CFStringRef contentTypeUTI, _Nonnull CFURLRef url); + +// Plist +NSData * _Nullable readPayloadFile(QuickLookInfo meta, NSString * _Nonnull filename); +NSDictionary * _Nullable readPlistApp(QuickLookInfo meta); +NSDictionary * _Nullable readPlistProvision(QuickLookInfo meta); +NSDictionary * _Nullable readPlistItunes(QuickLookInfo meta); + +// Other helper +typedef NS_ENUM(NSUInteger, ExpirationStatus) { + ExpirationStatusExpired = 0, + ExpirationStatusExpiring = 1, + ExpirationStatusValid = 2, +}; +ExpirationStatus expirationStatus(NSDate * _Nullable date); +NSDate * _Nullable dateOrNil(NSDate * _Nullable value); +NSArray * _Nullable arrayOrNil(NSArray * _Nullable value); diff --git a/ProvisionQL/Shared.m b/ProvisionQL/Shared.m index 8155d4a..0d308db 100644 --- a/ProvisionQL/Shared.m +++ b/ProvisionQL/Shared.m @@ -1,144 +1,136 @@ #import "Shared.h" +#import "ZipFile.h" -NSImage *roundCorners(NSImage *image) { - NSImage *existingImage = image; - NSSize existingSize = [existingImage size]; - NSImage *composedImage = [[NSImage alloc] initWithSize:existingSize]; +// MARK: - Meta data for QuickLook - [composedImage lockFocus]; - [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh]; +/// Search an archive for the .app or .ipa bundle. +NSURL * _Nullable appPathForArchive(NSURL *url) { + NSURL *appsDir = [url URLByAppendingPathComponent:@"Products/Applications/"]; + if (appsDir != nil) { + NSArray *dirFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:appsDir.path error:nil]; + if (dirFiles.count > 0) { + return [appsDir URLByAppendingPathComponent:dirFiles[0] isDirectory:YES]; + } + } + return nil; +} - NSRect imageFrame = NSRectFromCGRect(CGRectMake(0, 0, existingSize.width, existingSize.height)); - NSBezierPath *clipPath = [NSBezierPath bezierPathWithIOS7RoundedRect:imageFrame cornerRadius:existingSize.width * 0.225]; - [clipPath setWindingRule:NSWindingRuleEvenOdd]; - [clipPath addClip]; +/// Use file url and UTI type to generate an info object to pass around. +QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { + QuickLookInfo data = {}; + data.UTI = (__bridge NSString *)contentTypeUTI; + data.url = (__bridge NSURL *)url; + + if ([data.UTI isEqualToString:kDataType_ipa] + // for now, treat .tipa as if it were a normal .ipa file. + || [data.UTI isEqualToString:kDataType_trollstore_ipa] || [data.UTI isEqualToString:kDataType_trollstore_ipa_dyn]) + { + data.type = FileTypeIPA; + data.zipFile = [ZipFile open:data.url.path]; + } else if ([data.UTI isEqualToString:kDataType_xcode_archive]) { + data.type = FileTypeArchive; + data.effectiveUrl = appPathForArchive(data.url); + } else if ([data.UTI isEqualToString:kDataType_app_extension]) { + data.type = FileTypeExtension; + } else if ([data.UTI isEqualToString:kDataType_ios_provision]) { + data.type = FileTypeProvision; + } else if ([data.UTI isEqualToString:kDataType_ios_provision_old]) { + data.type = FileTypeProvision; + } else if ([data.UTI isEqualToString:kDataType_osx_provision]) { + data.type = FileTypeProvision; + data.isOSX = YES; + } + return data; +} - [image drawAtPoint:NSZeroPoint fromRect:NSMakeRect(0, 0, existingSize.width, existingSize.height) operation:NSCompositingOperationSourceOver fraction:1]; +/// Load a file from bundle into memory. Either by file path or via unzip. +NSData * _Nullable readPayloadFile(QuickLookInfo meta, NSString *filename) { + switch (meta.type) { + case FileTypeIPA: return [meta.zipFile unzipFile:[@"Payload/*.app/" stringByAppendingString:filename]]; + case FileTypeArchive: return [NSData dataWithContentsOfURL:[meta.effectiveUrl URLByAppendingPathComponent:filename]]; + case FileTypeExtension: return [NSData dataWithContentsOfURL:[meta.url URLByAppendingPathComponent:filename]]; + case FileTypeProvision: return nil; + } + return nil; +} - [composedImage unlockFocus]; +// MARK: - Plist - return composedImage; +/// Helper for optional chaining. +NSDictionary * _Nullable asPlistOrNil(NSData * _Nullable data) { + if (!data) { return nil; } + NSError *err; + NSDictionary *dict = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:&err]; + if (err) { + NSLog(@"ERROR reading plist %@", err); + return nil; + } + return dict; } -int expirationStatus(NSDate *date, NSCalendar *calendar) { - int result = 0; - - if (date) { - NSDateComponents *dateComponents = [calendar components:NSCalendarUnitDay fromDate:[NSDate date] toDate:date options:0]; - if ([date compare:[NSDate date]] == NSOrderedAscending) { - // expired - result = 0; - } else if (dateComponents.day < 30) { - // expiring - result = 1; - } else { - // valid - result = 2; +/// Read app default @c Info.plist. +NSDictionary * _Nullable readPlistApp(QuickLookInfo meta) { + switch (meta.type) { + case FileTypeIPA: + case FileTypeArchive: + case FileTypeExtension: { + return asPlistOrNil(readPayloadFile(meta, @"Info.plist")); } + case FileTypeProvision: + return nil; } - - return result; + return nil; } -NSImage *imageFromApp(NSURL *URL, NSString *dataType, NSString *fileName) { - NSImage *appIcon = nil; - - if ([dataType isEqualToString:kDataType_xcode_archive]) { - // get the embedded icon for the iOS app - NSURL *appsDir = [URL URLByAppendingPathComponent:@"Products/Applications/"]; - if (!appsDir) { - return nil; - } - - NSArray *dirFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:appsDir.path error:nil]; - NSString *appName = dirFiles.firstObject; - if (!appName) { - return nil; - } - - NSURL *appURL = [appsDir URLByAppendingPathComponent:appName]; - NSArray *appContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:appURL.path error:nil]; - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF contains %@", fileName]; - NSString *appIconFullName = [appContents filteredArrayUsingPredicate:predicate].lastObject; - if (!appIconFullName) { - return nil; - } - - NSURL *appIconFullURL = [appURL URLByAppendingPathComponent:appIconFullName]; - appIcon = [[NSImage alloc] initWithContentsOfURL:appIconFullURL]; - } else if([dataType isEqualToString:kDataType_ipa]) { - // get the embedded icon from an app arcive using: unzip -p 'Payload/*.app/' (piped to standard output) - NSTask *unzipTask = [NSTask new]; - [unzipTask setLaunchPath:@"/usr/bin/unzip"]; - [unzipTask setStandardOutput:[NSPipe pipe]]; - [unzipTask setArguments:@[@"-p", [URL path], [NSString stringWithFormat:@"Payload/*.app/%@*", fileName], @"-x", @"*/*/*/*"]]; - [unzipTask launch]; - - NSData *pipeData = [[[unzipTask standardOutput] fileHandleForReading] readDataToEndOfFile]; - [unzipTask waitUntilExit]; - - appIcon = [[NSImage alloc] initWithData:pipeData]; - } +/// Read @c embedded.mobileprovision file and decode with CMS decoder. +NSDictionary * _Nullable readPlistProvision(QuickLookInfo meta) { + NSData *provisionData; + if (meta.type == FileTypeProvision) { + provisionData = [NSData dataWithContentsOfURL:meta.url]; // the target file itself + } else { + provisionData = readPayloadFile(meta, @"embedded.mobileprovision"); + } + if (!provisionData) { + NSLog(@"No provisionData for %@", meta.url); + return nil; + } - return appIcon; + CMSDecoderRef decoder = NULL; + CMSDecoderCreate(&decoder); + CMSDecoderUpdateMessage(decoder, provisionData.bytes, provisionData.length); + CMSDecoderFinalizeMessage(decoder); + CFDataRef dataRef = NULL; + CMSDecoderCopyContent(decoder, &dataRef); + NSData *data = (NSData *)CFBridgingRelease(dataRef); + CFRelease(decoder); + return asPlistOrNil(data); } -NSArray *iconsListForDictionary(NSDictionary *iconsDict) { - if ([iconsDict isKindOfClass:[NSDictionary class]]) { - id primaryIconDict = [iconsDict objectForKey:@"CFBundlePrimaryIcon"]; - if ([primaryIconDict isKindOfClass:[NSDictionary class]]) { - id tempIcons = [primaryIconDict objectForKey:@"CFBundleIconFiles"]; - if ([tempIcons isKindOfClass:[NSArray class]]) { - return tempIcons; - } - } - } - - return nil; +/// Read @c iTunesMetadata.plist if available +NSDictionary * _Nullable readPlistItunes(QuickLookInfo meta) { + if (meta.type == FileTypeIPA) { + return asPlistOrNil([meta.zipFile unzipFile:@"iTunesMetadata.plist"]); + } + return nil; } -NSString *mainIconNameForApp(NSDictionary *appPropertyList) { - NSArray *icons; - NSString *iconName; +// MARK: - Other helper - //Check for CFBundleIcons (since 5.0) - icons = iconsListForDictionary([appPropertyList objectForKey:@"CFBundleIcons"]); - if (!icons) { - icons = iconsListForDictionary([appPropertyList objectForKey:@"CFBundleIcons~ipad"]); - } - - if (!icons) { - //Check for CFBundleIconFiles (since 3.2) - id tempIcons = [appPropertyList objectForKey:@"CFBundleIconFiles"]; - if ([tempIcons isKindOfClass:[NSArray class]]) { - icons = tempIcons; - } - } - - if (icons) { - //Search some patterns for primary app icon (120x120) - NSArray *matches = @[@"120",@"60"]; - - for (NSString *match in matches) { - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF contains[c] %@",match]; - NSArray *results = [icons filteredArrayUsingPredicate:predicate]; - if ([results count]) { - iconName = [results firstObject]; - break; - } - } +/// Check time between date and now. Set Expiring if less than 30 days until expiration +ExpirationStatus expirationStatus(NSDate *date) { + if (!date || [date compare:[NSDate date]] == NSOrderedAscending) { + return ExpirationStatusExpired; + } + NSDateComponents *dateComponents = [[NSCalendar currentCalendar] components:NSCalendarUnitDay fromDate:[NSDate date] toDate:date options:0]; + return dateComponents.day < 30 ? ExpirationStatusExpiring : ExpirationStatusValid; +} - //If no one matches any pattern, just take last item - if (!iconName) { - iconName = [icons lastObject]; - } - } else { - //Check for CFBundleIconFile (legacy, before 3.2) - NSString *legacyIcon = [appPropertyList objectForKey:@"CFBundleIconFile"]; - if ([legacyIcon length]) { - iconName = legacyIcon; - } - } +/// Ensures the value is of type @c NSDate +inline NSDate * _Nullable dateOrNil(NSDate * _Nullable value) { + return [value isKindOfClass:[NSDate class]] ? value : nil; +} - return iconName; +/// Ensures the value is of type @c NSArray +inline NSArray * _Nullable arrayOrNil(NSArray * _Nullable value) { + return [value isKindOfClass:[NSArray class]] ? value : nil; } diff --git a/ProvisionQL/Supporting-files/Info.plist b/ProvisionQL/Supporting-files/Info.plist index bf2605b..140dea4 100644 --- a/ProvisionQL/Supporting-files/Info.plist +++ b/ProvisionQL/Supporting-files/Info.plist @@ -17,6 +17,8 @@ com.apple.provisionprofile com.apple.application-and-system-extension com.apple.xcode.archive + com.opa334.trollstore.tipa + dyn.ah62d4rv4ge81k4puqe @@ -62,5 +64,27 @@ QLThumbnailMinimumSize 16 + UTImportedTypeDeclarations + + + UTTypeIdentifier + com.opa334.trollstore.tipa + UTTypeDescription + AirDrop friendly iOS app + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + public.filename-extension + + tipa + + public.mime-type + application/trollstore-ipa + + + diff --git a/ProvisionQL/ZipFile.h b/ProvisionQL/ZipFile.h new file mode 100644 index 0000000..c178423 --- /dev/null +++ b/ProvisionQL/ZipFile.h @@ -0,0 +1,13 @@ +#import +@class ZipEntry; + +NS_ASSUME_NONNULL_BEGIN + +@interface ZipFile : NSObject ++ (instancetype)open:(NSString *)path; +- (NSData * _Nullable)unzipFile:(NSString *)filePath; +- (void)unzipFile:(NSString *)filePath toDir:(NSString *)targetDir; +- (NSArray * _Nullable)filesMatching:(NSString * _Nonnull)path; +@end + +NS_ASSUME_NONNULL_END diff --git a/ProvisionQL/ZipFile.m b/ProvisionQL/ZipFile.m new file mode 100644 index 0000000..4ad3727 --- /dev/null +++ b/ProvisionQL/ZipFile.m @@ -0,0 +1,115 @@ +#import "ZipFile.h" +#import "pinch.h" + +@interface ZipFile() +@property (nonatomic, retain, readonly) NSString * pathToZipFile; +@property (nonatomic, retain, readonly, nullable) NSArray *centralDirectory; +@end + + +@implementation ZipFile + ++ (instancetype)open:(NSString *)path { + return [[self alloc] initWithFile:path]; +} + +- (instancetype)initWithFile:(NSString *)path { + self = [super init]; + if (self) { + _pathToZipFile = path; + _centralDirectory = listZip(path); + } + return self; +} + + +// MARK: - public methods + +- (NSArray * _Nullable)filesMatching:(NSString * _Nonnull)path { + if (self.centralDirectory) { + NSPredicate *pred = [NSPredicate predicateWithFormat:@"filepath LIKE %@", path]; + return [self.centralDirectory filteredArrayUsingPredicate:pred]; + } + return nil; +} + +/// Unzip file directly into memory. +/// @param filePath File path inside zip file. +- (NSData * _Nullable)unzipFile:(NSString *)filePath { + if (self.centralDirectory) { + ZipEntry *matchingFile = [self.centralDirectory zipEntryWithPath:filePath]; + if (!matchingFile) { +#ifdef DEBUG + NSLog(@"[unzip] cant find '%@'", filePath); +#endif + // There is a dir listing but no matching file. + // This means there wont be anything to extract. + // Not even a sys-call can help here. + return nil; + } +#ifdef DEBUG + NSLog(@"[unzip] %@", matchingFile.filepath); +#endif + NSData *data = unzipFileEntry(self.pathToZipFile, matchingFile); + if (data) { + return data; + } + } + // fallback to sys unzip + return [self sysUnzipFile:filePath]; +} + +/// Unzip file to filesystem. +/// @param filePath File path inside zip file. +/// @param targetDir Directory in which to unzip the file. +- (void)unzipFile:(NSString *)filePath toDir:(NSString *)targetDir { + if (self.centralDirectory) { + NSData *data = [self unzipFile:filePath]; + if (data) { + NSString *outputPath = [targetDir stringByAppendingPathComponent:[filePath lastPathComponent]]; +#ifdef DEBUG + NSLog(@"[unzip] write to %@", outputPath); +#endif + [data writeToFile:outputPath atomically:NO]; + return; + } + } + [self sysUnzipFile:filePath toDir:targetDir]; +} + + +// MARK: - fallback to sys call + +- (NSData * _Nullable)sysUnzipFile:(NSString *)filePath { + NSTask *task = [NSTask new]; + [task setLaunchPath:@"/usr/bin/unzip"]; + [task setStandardOutput:[NSPipe pipe]]; + [task setArguments:@[@"-p", self.pathToZipFile, filePath]]; // @"-x", @"*/*/*/*" + [task launch]; + +#ifdef DEBUG + NSLog(@"[sys-call] unzip %@", [[task arguments] componentsJoinedByString:@" "]); +#endif + + NSData *pipeData = [[[task standardOutput] fileHandleForReading] readDataToEndOfFile]; + [task waitUntilExit]; + if (pipeData.length == 0) { + return nil; + } + return pipeData; +} + +- (void)sysUnzipFile:(NSString *)filePath toDir:(NSString *)targetDir { + NSTask *task = [NSTask new]; + [task setLaunchPath:@"/usr/bin/unzip"]; + [task setArguments:@[@"-u", @"-j", @"-d", targetDir, self.pathToZipFile, filePath]]; // @"-x", @"*/*/*/*" + [task launch]; + +#ifdef DEBUG + NSLog(@"[sys-call] unzip %@", [[task arguments] componentsJoinedByString:@" "]); +#endif + + [task waitUntilExit]; +} + +@end