From 90896634fe9803190bbb2efdc081be3909de9557 Mon Sep 17 00:00:00 2001 From: relikd Date: Thu, 25 Jan 2024 19:28:45 +0100 Subject: [PATCH 01/35] ref: full rewrite with modular processing --- ProvisionQL/GeneratePreviewForURL.m | 1320 ++++++++++++------------- ProvisionQL/GenerateThumbnailForURL.m | 308 +++--- ProvisionQL/Resources/template.html | 13 +- ProvisionQL/Shared.h | 63 +- ProvisionQL/Shared.m | 339 ++++--- 5 files changed, 1033 insertions(+), 1010 deletions(-) diff --git a/ProvisionQL/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index e3adc71..de631ed 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -1,5 +1,8 @@ #import "Shared.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,722 +12,665 @@ 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); - - 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; - displayKeyAndValue(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) { - displayKeyAndValue(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]; - } - } +// MARK: - Generic data formatting & printing + +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; } -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; +/// 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]; + } + } } -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]; +/// 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) ? @"- " : @"  "]; + } + + 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]; + } + } } -NSDictionary *formattedDevicesData(NSArray *value) { +/// 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; +} - NSArray *array = (NSArray *)value; - NSArray *sortedArray = [array sortedArrayUsingSelector:@selector(compare:)]; +/// Convert image to PNG and encode with base64 to be embeded in html output. +NSString * _Nonnull iconAsBase64(NSImage *appIcon) { + appIcon = roundCorners(appIcon); + NSData *imageData = [appIcon TIFFRepresentation]; + NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData]; + imageData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; + return [imageData base64EncodedStringWithOptions:0]; +} - NSString *currentPrefix = nil; - NSMutableString *devices = [NSMutableString string]; - [devices appendString:@"\n"]; - [devices appendString:@"\n"]; - 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]; - } +// MARK: - Date processing - [devices appendFormat:@"\n", displayPrefix, device]; - } - [devices appendString:@"
UDID
%@%@
\n"]; +/// @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 @{@"ProvisionedDevicesFormatted" : [devices copy], @"ProvisionedDevicesCount" : [NSString stringWithFormat:@"%zd Device%s", [array count], ([array count] == 1 ? "" : "s")]}; +/// @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]; } -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; +/// @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]; } -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; +/// @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)]; } -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]]; - if (@available(macOS 11, *)) { - [codesignTask setArguments:@[@"-d", binaryPath, @"--entitlements", @"-", @"--xml"]]; - } else { - [codesignTask setArguments:@[@"-d", binaryPath, @"--entitlements", @":-"]]; - } - [codesignTask launch]; - - NSData *outputData = [[[codesignTask standardOutput] fileHandleForReading] readDataToEndOfFile]; - NSData *errorData = [[[codesignTask standardError] fileHandleForReading] readDataToEndOfFile]; - [codesignTask waitUntilExit]; - - if (outputData.length == 0) { - return errorData; - } - - return outputData; +/// @return Relative distance to today. E.g., "DATE (Expires in 3 days)" +NSString * _Nonnull formattedExpirationDate(NSDate *expireDate) { + return [NSString stringWithFormat:@"%@ (%@)", formattedDate(expireDate), relativeExpirationDateString(expireDate)]; } -NSString *iconAsBase64(NSImage *appIcon) { - 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:@{}]; - return [imageData base64EncodedStringWithOptions:0]; +/// @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)]]; } -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]) { - provisionData = unzipFile(URL, @"Payload/*.app/embedded.mobileprovision"); - appPlist = unzipFile(URL, @"Payload/*.app/Info.plist"); - - // 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"]; - - unzipFileToDir(URL, currentTempDirFolder, [@"Payload/*.app/" stringByAppendingPathComponent:bundleExecutable]); - - 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 *iconName = mainIconNameForApp(appPropertyList); - appIcon = imageFromApp(URL, dataType, iconName); - [synthesizedInfo setObject:iconAsBase64(appIcon) forKey:@"AppIcon"]; - - 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"]; - - 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"]; - [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 +/// @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"; + } +} -#ifdef DEBUG - [synthesizedInfo setObject:@"(debug)" forKey:@"DEBUG"]; -#else - [synthesizedInfo setObject:@"" forKey:@"DEBUG"]; -#endif - synthesizedValue = [[NSBundle bundleWithIdentifier:kPluginBundleId] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; - [synthesizedInfo setObject:synthesizedValue ?: @"" forKey:@"BundleShortVersionString"]; +// 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 @{ + @"AppInfo": @"hiddenDiv", + @"ProvisionAsSubheader": @"", + }; + } + + NSString *bundleName = appPlist[@"CFBundleDisplayName"]; + if (!bundleName) { + bundleName = appPlist[@"CFBundleName"]; + } + 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; + } + } + + return @{ + @"AppInfo": @"", + @"ProvisionAsSubheader": @"hiddenDiv", + + @"CFBundleName": appPlist[@"CFBundleDisplayName"] ?: appPlist[@"CFBundleName"] ?: @"", + @"CFBundleShortVersionString": appPlist[@"CFBundleShortVersionString"] ?: @"", + @"CFBundleVersion": appPlist[@"CFBundleVersion"] ?: @"", + @"CFBundleIdentifier": appPlist[@"CFBundleIdentifier"] ?: @"", + + @"ExtensionInfo": extensionType ? @"" : @"hiddenDiv", + @"NSExtensionPointIdentifier": extensionType ?: @"", + + @"UIDeviceFamily": [platforms componentsJoinedByString:@", "], + @"DTSDKName": appPlist[@"DTSDKName"] ?: @"", + @"MinimumOSVersion": appPlist[@"MinimumOSVersion"] ?: @"", + @"AppTransportSecurityFormatted": formattedAppTransportSecurity(appPlist), + }; +} + + +// 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 && !error) { + 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) { + if ([value isKindOfClass:[NSDate class]]) { + invalidityDate = value; + } else { + // parse the date from a string + NSDateFormatter *dateFormatter = [NSDateFormatter new]; + [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"]; + invalidityDate = [dateFormatter dateFromString:[value description]]; + } + } else { + NSLog(@"No invalidity date in '%@' certificate, dictionary = %@", subject, innerDictRef); + } + // no CFRelease(innerDictRef); since it has the same references as outerDictRef + } else { + NSLog(@"No invalidity values in '%@' certificate, dictionary = %@", subject, outerDictRef); + } + CFRelease(outerDictRef); + } else { + 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 { + NSLog(@"Could not get subject from certificate"); + } + CFRelease(certificateRef); + } + + [entries sortUsingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) { + return [obj1[0] compare:obj2[0]]; + }]; + return entries; +} + + +// 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)"; +} + +/// 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 @{ + @"ProvisionInfo": @"hiddenDiv", + }; + } + + NSDate *creationDate = dateOrNil(provisionPlist[@"CreationDate"]); + NSDate *expireDate = dateOrNil(provisionPlist[@"ExpirationDate"]); + NSArray* devices = getDeviceList(provisionPlist); + + return @{ + @"ProvisionInfo": @"", + @"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", + }; +} + - synthesizedValue = [[NSBundle bundleWithIdentifier:kPluginBundleId] objectForInfoDictionaryKey:@"CFBundleVersion"]; - [synthesizedInfo setObject:synthesizedValue ?: @"" forKey:@"BundleVersion"]; +// MARK: - Entitlements + +/// run: @c codesign -d --entitlements - --xml +NSData *runCodeSign(NSString *binaryPath) { + 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]; + + NSData *outputData = [[[codesignTask standardOutput] fileHandleForReading] readDataToEndOfFile]; + NSData *errorData = [[[codesignTask standardError] fileHandleForReading] readDataToEndOfFile]; + [codesignTask waitUntilExit]; + + if (outputData.length == 0) { + return errorData; + } + return outputData; +} - 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])]; - } +/// Search for app binary and run @c codesign on it. +NSData *getCodeSignEntitlements(QuickLookInfo meta, NSString *bundleExecutable) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *tempDirFolder = [NSTemporaryDirectory() stringByAppendingPathComponent:kPluginBundleId]; + NSString *currentTempDirFolder = [tempDirFolder stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; + + NSString *basePath = nil; + switch (meta.type) { + case FileTypeIPA: + basePath = currentTempDirFolder; + [fileManager createDirectoryAtPath:currentTempDirFolder withIntermediateDirectories:YES attributes:nil error:nil]; + unzipFileToDir(meta.url, currentTempDirFolder, [@"Payload/*.app/" stringByAppendingPathComponent:bundleExecutable]); + break; + case FileTypeArchive: + basePath = meta.effectiveUrl.path; + break; + case FileTypeExtension: + basePath = meta.url.path; + break; + case FileTypeProvision: + return nil; + } + + NSData *data = runCodeSign([basePath stringByAppendingPathComponent:bundleExecutable]); + [fileManager removeItemAtPath:currentTempDirFolder error:nil]; + return data; +} - NSDictionary *properties = @{ // properties for the HTML data - (__bridge NSString *)kQLPreviewPropertyTextEncodingNameKey : @"UTF-8", - (__bridge NSString *)kQLPreviewPropertyMIMETypeKey : @"text/html" }; +/// Print formatted plist in a @c \
 tag
+NSString * _Nonnull formattedPlist(NSDictionary *dict) {
+	NSMutableString *output = [NSMutableString string];
+	recursiveKeyValue(0, nil, dict, output);
+	return [NSString stringWithFormat:@"
%@
", output]; +} + +/// First, try to extract real entitlements by running codesign. +/// If that fails, fallback to entitlements provided by provision plist. +NSDictionary * _Nonnull procEntitlements(NSData *codeSignData, NSDictionary *provisionPlist) { + BOOL showEntitlementsWarning = false; + NSString *formattedOutput = nil; + if (codeSignData != nil) { + NSDictionary *plist = [NSPropertyListSerialization propertyListWithData:codeSignData options:0 format:NULL error:NULL]; + if (plist != nil) { + formattedOutput = formattedPlist(plist); + } else { + showEntitlementsWarning = true; + NSString *output = [[NSString alloc] initWithData:codeSignData encoding:NSUTF8StringEncoding]; + if ([output hasPrefix:@"Executable="]) { + // remove first line with long temporary path to the executable + NSArray *allLines = [output componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + formattedOutput = [[allLines subarrayWithRange:NSMakeRange(1, allLines.count - 1)] componentsJoinedByString:@"
"]; + } else { + formattedOutput = output; + } + } + } else { + // read the entitlements from the provisioning profile instead + NSDictionary *value = provisionPlist[@"Entitlements"]; + if ([value isKindOfClass:[NSDictionary class]]) { + formattedOutput = formattedPlist(value); + } else { + formattedOutput = @"No Entitlements"; + } + } + + return @{ + @"EntitlementsFormatted": formattedOutput ?: @"", + @"EntitlementsWarning": showEntitlementsWarning ? @"" : @"hiddenDiv", + }; +} - QLPreviewRequestSetDataRepresentation(preview, (__bridge CFDataRef)[html dataUsingEncoding:NSUTF8StringEncoding], kUTTypeHTML, (__bridge CFDictionaryRef)properties); - } - return noErr; +// 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; + } +} + +/// 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; +} + +/// 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 ?: @"", + }; +} + + +// 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 + @"DEBUG": @"(debug)", +#else + @"DEBUG": @"", +#endif + @"BundleShortVersionString": [mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"", + @"BundleVersion": [mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"] ?: @"", + }; +} + + +// 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) { + NSLog(@"WARN: unused key %@", key); + } else { + [rv appendString:value]; + } + prevLoc = start + result.range.length; + }]; + [rv appendString:[html substringWithRange:NSMakeRange(prevLoc, html.length - prevLoc)]]; + return rv; +} + +OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options) { + @autoreleasepool { + QuickLookInfo meta = initQLInfo(contentTypeUTI, url); + NSMutableDictionary* infoLayer = [NSMutableDictionary dictionary]; + infoLayer[@"AppInfoTitle"] = stringForFileType(meta); + + // App Info + NSDictionary *plistApp = readPlistApp(meta); + ALLOW_EXIT + + [infoLayer addEntriesFromDictionary:procAppInfo(plistApp)]; + ALLOW_EXIT + + // Provisioning + NSDictionary *plistProvision = readPlistProvision(meta); + ALLOW_EXIT + + if (!plistApp && !plistProvision) { + return noErr; // nothing to do. Maybe another QL plugin can do better. + } + + [infoLayer addEntriesFromDictionary:procProvision(plistProvision, meta.isOSX)]; + ALLOW_EXIT + + // App Icon + infoLayer[@"AppIcon"] = iconAsBase64(imageFromApp(meta, plistApp)); + ALLOW_EXIT + + // Entitlements + NSString *bundleExecutable = plistApp[@"CFBundleExecutable"]; + NSData *codeSignData = getCodeSignEntitlements(meta, bundleExecutable); + ALLOW_EXIT + + [infoLayer addEntriesFromDictionary:procEntitlements(codeSignData, plistProvision)]; + ALLOW_EXIT + + // File Info + [infoLayer addEntriesFromDictionary:procFileInfo(meta.url)]; + ALLOW_EXIT + + // Footer Info + [infoLayer addEntriesFromDictionary:procFooterInfo()]; + ALLOW_EXIT + + // prepare html, replace values + NSString *html = applyHtmlTemplate(infoLayer); + ALLOW_EXIT + + // 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 + // Implement only if supported } diff --git a/ProvisionQL/GenerateThumbnailForURL.m b/ProvisionQL/GenerateThumbnailForURL.m index 1e4989e..ba5f008 100644 --- a/ProvisionQL/GenerateThumbnailForURL.m +++ b/ProvisionQL/GenerateThumbnailForURL.m @@ -1,5 +1,8 @@ #import "Shared.h" +// makro to stop further processing +#define ALLOW_EXIT if (QLThumbnailRequestIsCancelled(thumbnail)) { return noErr; } + //Layout constants #define BADGE_MARGIN 10.0 #define MIN_BADGE_WIDTH 40.0 @@ -19,182 +22,143 @@ 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 .xarchive + +OSStatus renderAppIcon(QuickLookInfo meta, QLThumbnailRequestRef thumbnail) { + NSImage *appIcon = imageFromApp(meta, nil); + ALLOW_EXIT + + // downscale as required by QLThumbnailRequestSetImageWithData + CGSize maxSize = QLThumbnailRequestGetMaximumSize(thumbnail); + if (appIcon.size.width > maxSize.width && appIcon.size.height > maxSize.height) { + [appIcon setSize:maxSize]; + } + + appIcon = roundCorners(appIcon); + ALLOW_EXIT + + // 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 + } + + // 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); + ALLOW_EXIT + + // 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; - NSData *appPlist = nil; - NSImage *appIcon = nil; - - 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]) { - appPlist = unzipFile(URL, @"Payload/*.app/Info.plist"); - } - - if (QLThumbnailRequestIsCancelled(thumbnail)) { - return noErr; - } - - // MARK: .ipa & .xarchive - - 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 (QLThumbnailRequestIsCancelled(thumbnail)) { - return noErr; - } - - if (!appIcon) { - NSURL *iconURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"defaultIcon" withExtension:@"png"]; - appIcon = [[NSImage alloc] initWithContentsOfURL:iconURL]; - } - static const NSString *IconFlavor; - if (@available(macOS 10.15, *)) { - IconFlavor = @"icon"; - } else { - IconFlavor = @"IconFlavor"; - } - NSDictionary *propertiesDict = nil; - if ([dataType isEqualToString:kDataType_xcode_archive]) { - // 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)}; - } else { - propertiesDict = @{IconFlavor : @(0)}; - } - // image-only icons can be drawn efficiently. - appIcon = roundCorners(appIcon); - // if downscale, then this should respect retina resolution -// if (appIcon.size.width > maxSize.width && appIcon.size.height > maxSize.height) { -// [appIcon setSize:maxSize]; -// } - QLThumbnailRequestSetImageWithData(thumbnail, (__bridge CFDataRef)[appIcon TIFFRepresentation], (__bridge CFDictionaryRef)propertiesDict); - return noErr; - } - - // MARK: .provisioning - - // use provisioning directly - NSData *provisionData = [NSData dataWithContentsOfURL:URL]; - if (!provisionData) { - NSLog(@"No provisionData for %@", URL); - return noErr; - } - - NSDictionary *optionsDict = (__bridge NSDictionary *)options; - BOOL iconMode = ([optionsDict objectForKey:(NSString *)kQLThumbnailOptionIconModeKey]) ? YES : NO; - NSUInteger devicesCount = 0; - int expStatus = 0; - - 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)]; - } - - 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, NULL); - if (_context) { - NSGraphicsContext *_graphicsContext = [NSGraphicsContext graphicsContextWithCGContext:(void *)_context flipped:NO]; - - [NSGraphicsContext setCurrentContext:_graphicsContext]; - - [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 == FileTypeIPA || meta.type == FileTypeArchive) { + return renderAppIcon(meta, thumbnail); + } else if (meta.type == FileTypeProvision) { + NSDictionary *optionsDict = (__bridge NSDictionary *)options; + BOOL iconMode = ([optionsDict objectForKey:(NSString *)kQLThumbnailOptionIconModeKey]) ? YES : NO; + return renderProvision(meta, thumbnail, iconMode); + } + } + return noErr; } void CancelPreviewGeneration(void *thisInterface, QLPreviewRequestRef preview) { - // Implement only if supported + // Implement only if supported } diff --git a/ProvisionQL/Resources/template.html b/ProvisionQL/Resources/template.html index 340ce41..2d04525 100644 --- a/ProvisionQL/Resources/template.html +++ b/ProvisionQL/Resources/template.html @@ -58,6 +58,7 @@ h2 { font-size: 14px; + margin-top: 4ex; text-transform: uppercase; } @@ -158,18 +159,18 @@

App Transport Security

-

Provisioning

- Profile name: __Name__
+

Provisioning

+ Profile name: __ProfileName__
-

__Name__

+

__ProfileName__

- Profile UUID: __UUID__
+ 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

diff --git a/ProvisionQL/Shared.h b/ProvisionQL/Shared.h index 3f48fec..a39ba90 100644 --- a/ProvisionQL/Shared.h +++ b/ProvisionQL/Shared.h @@ -8,18 +8,51 @@ #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"; - -NSData *unzipFile(NSURL *url, NSString *filePath); -void unzipFileToDir(NSURL *url, NSString *filePath, NSString *targetDir); - -NSImage *roundCorners(NSImage *image); -NSImage *imageFromApp(NSURL *URL, NSString *dataType, NSString *fileName); -NSString *mainIconNameForApp(NSDictionary *appPropertyList); -int expirationStatus(NSDate *date, NSCalendar *calendar); +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"; + +// 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; +} QuickLookInfo; + +QuickLookInfo initQLInfo(_Nonnull CFStringRef contentTypeUTI, _Nonnull CFURLRef url); + + +// Unzip +void unzipFileToDir(NSURL * _Nonnull url, NSString * _Nonnull filePath, NSString * _Nonnull targetDir); + +// Plist +NSDictionary * _Nullable readPlistApp(QuickLookInfo meta); +NSDictionary * _Nullable readPlistProvision(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); + +// App Icon +NSImage * _Nonnull roundCorners(NSImage * _Nonnull image); +NSImage * _Nonnull imageFromApp(QuickLookInfo meta, NSDictionary * _Nullable appPlist); diff --git a/ProvisionQL/Shared.m b/ProvisionQL/Shared.m index b6eeca5..7eaa4c8 100644 --- a/ProvisionQL/Shared.m +++ b/ProvisionQL/Shared.m @@ -1,29 +1,154 @@ #import "Shared.h" -NSData *unzipFile(NSURL *url, NSString *filePath) { - NSTask *task = [NSTask new]; - [task setLaunchPath:@"/usr/bin/unzip"]; - [task setStandardOutput:[NSPipe pipe]]; - [task setArguments:@[@"-p", [url path], filePath]]; // @"-x", @"*/*/*/*" - [task launch]; - - NSData *pipeData = [[[task standardOutput] fileHandleForReading] readDataToEndOfFile]; - [task waitUntilExit]; - if (pipeData.length == 0) { - return nil; - } - return pipeData; +// MARK: - Meta data for QuickLook + +/// 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; } +/// 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]) { + data.type = FileTypeIPA; + } 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; +} + + +// MARK: Unzip + +/// Unzip file directly into memory. +NSData * _Nullable unzipFile(NSURL *url, NSString *filePath) { + NSTask *task = [NSTask new]; + [task setLaunchPath:@"/usr/bin/unzip"]; + [task setStandardOutput:[NSPipe pipe]]; + [task setArguments:@[@"-p", [url path], filePath]]; // @"-x", @"*/*/*/*" + [task launch]; + + NSData *pipeData = [[[task standardOutput] fileHandleForReading] readDataToEndOfFile]; + [task waitUntilExit]; + if (pipeData.length == 0) { + return nil; + } + return pipeData; +} + +/// Unzip file to filesystem. void unzipFileToDir(NSURL *url, NSString *targetDir, NSString *filePath) { - NSTask *task = [NSTask new]; - [task setLaunchPath:@"/usr/bin/unzip"]; - [task setArguments:@[@"-u", @"-j", @"-d", targetDir, [url path], filePath]]; // @"-x", @"*/*/*/*" - [task launch]; - [task waitUntilExit]; + NSTask *task = [NSTask new]; + [task setLaunchPath:@"/usr/bin/unzip"]; + [task setArguments:@[@"-u", @"-j", @"-d", targetDir, [url path], filePath]]; // @"-x", @"*/*/*/*" + [task launch]; + [task waitUntilExit]; +} + +/// 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 unzipFile(meta.url, [@"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; + } +} + + +// MARK: Plist + +/// Read app default @c Info.plist. +NSDictionary * _Nullable readPlistApp(QuickLookInfo meta) { + NSLog(@"read once"); + switch (meta.type) { + case FileTypeIPA: + case FileTypeArchive: + case FileTypeExtension: { + NSData *plistData = readPayloadFile(meta, @"Info.plist"); + return [NSPropertyListSerialization propertyListWithData:plistData options:0 format:NULL error:NULL]; + } + case FileTypeProvision: + return nil; + } } -NSImage *roundCorners(NSImage *image) { +/// 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; + } + + 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) { + return nil; + } + return [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL]; +} + + +// MARK: - Other helper + +/// 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; +} + +/// Ensures the value is of type @c NSDate +NSDate * _Nullable dateOrNil(NSDate * _Nullable value) { + return [value isKindOfClass:[NSDate class]] ? value : nil; +} + +/// Ensures the value is of type @c NSArray +NSArray * _Nullable arrayOrNil(NSArray * _Nullable value) { + return [value isKindOfClass:[NSArray class]] ? value : nil; +} + + +// MARK: - App Icon + +/// Apply rounded corners to image (iOS7 style) +NSImage * _Nonnull roundCorners(NSImage *image) { NSImage *existingImage = image; NSSize existingSize = [existingImage size]; NSImage *composedImage = [[NSImage alloc] initWithSize:existingSize]; @@ -43,121 +168,75 @@ void unzipFileToDir(NSURL *url, NSString *targetDir, NSString *filePath) { return composedImage; } -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; - } - } - - return result; +/// Given a list of filenames, try to find the one with the highest resolution +NSString *selectBestIcon(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 + return [icons lastObject]; } -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]) { - NSData *data = unzipFile(URL, @"iTunesArtwork"); - if (!data && fileName.length > 0) { - data = unzipFile(URL, [NSString stringWithFormat:@"Payload/*.app/%@*", fileName]); - } - if (data != nil) { - appIcon = [[NSImage alloc] initWithData:data]; - } - } - - return appIcon; +/// Deep select icons from plist key @c CFBundleIcons and @c CFBundleIcons~ipad +NSArray * _Nullable iconsListForDictionary(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; + } + } + } + return nil; } -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; +/// Parse app plist to find the bundle icon filename. +NSString * _Nullable mainIconNameForApp(NSDictionary *appPlist) { + //Check for CFBundleIcons (since 5.0) + NSArray *icons = iconsListForDictionary(appPlist[@"CFBundleIcons"]); + if (!icons) { + icons = iconsListForDictionary(appPlist[@"CFBundleIcons~ipad"]); + if (!icons) { + //Check for CFBundleIconFiles (since 3.2) + icons = arrayOrNil(appPlist[@"CFBundleIconFiles"]); + if (!icons) { + //Check for CFBundleIconFile (legacy, before 3.2) + return appPlist[@"CFBundleIconFile"]; // may be nil + } + } + } + return selectBestIcon(icons); } -NSString *mainIconNameForApp(NSDictionary *appPropertyList) { - NSArray *icons; - NSString *iconName; - - //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; - } - } - - //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; - } - } - - return iconName; +/// Depending on the file type, find the icon within the bundle +/// @param appPlist If @c nil, will load plist on the fly (used for thumbnail) +NSImage * _Nonnull imageFromApp(QuickLookInfo meta, NSDictionary *appPlist) { + if (meta.type == FileTypeIPA) { + NSData *data = unzipFile(meta.url, @"iTunesArtwork"); + if (!data) { + NSString *fileName = mainIconNameForApp(appPlist ?: readPlistApp(meta)); + data = unzipFile(meta.url, [NSString stringWithFormat:@"Payload/*.app/%@*", fileName]); + } + if (data) { + return [[NSImage alloc] initWithData:data]; + } + } else if (meta.type == FileTypeArchive) { + // get the embedded icon for the iOS app + NSString *fileName = mainIconNameForApp(appPlist ?: readPlistApp(meta)); + NSArray *appContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:meta.effectiveUrl.path error:nil]; + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF contains %@", fileName]; + NSString *matchedName = [appContents filteredArrayUsingPredicate:predicate].lastObject; + if (matchedName) { + NSURL *appIconFullURL = [meta.effectiveUrl URLByAppendingPathComponent:matchedName]; + return [[NSImage alloc] initWithContentsOfURL:appIconFullURL]; + } + } + // Fallback to default icon + NSURL *iconURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"defaultIcon" withExtension:@"png"]; + return [[NSImage alloc] initWithContentsOfURL:iconURL]; } From 80cd07a9f6887fc9e734805a55bbe22a222d42f6 Mon Sep 17 00:00:00 2001 From: relikd Date: Thu, 25 Jan 2024 19:45:13 +0100 Subject: [PATCH 02/35] fix: move functions to corresponding class --- ProvisionQL/GeneratePreviewForURL.m | 2 +- ProvisionQL/GenerateThumbnailForURL.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ProvisionQL/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index de631ed..809bf3f 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -671,6 +671,6 @@ OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, return noErr; } -void CancelThumbnailGeneration(void *thisInterface, QLThumbnailRequestRef thumbnail) { +void CancelPreviewGeneration(void *thisInterface, QLPreviewRequestRef preview) { // Implement only if supported } diff --git a/ProvisionQL/GenerateThumbnailForURL.m b/ProvisionQL/GenerateThumbnailForURL.m index ba5f008..8f6e20d 100644 --- a/ProvisionQL/GenerateThumbnailForURL.m +++ b/ProvisionQL/GenerateThumbnailForURL.m @@ -159,6 +159,6 @@ OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thum return noErr; } -void CancelPreviewGeneration(void *thisInterface, QLPreviewRequestRef preview) { +void CancelThumbnailGeneration(void *thisInterface, QLThumbnailRequestRef thumbnail) { // Implement only if supported } From eb9ddcefdf90f9158422a5307aba786c63097e7d Mon Sep 17 00:00:00 2001 From: relikd Date: Thu, 25 Jan 2024 19:45:47 +0100 Subject: [PATCH 03/35] chore: reduce polling --- ProvisionQL/GeneratePreviewForURL.m | 7 ------- ProvisionQL/GenerateThumbnailForURL.m | 1 - 2 files changed, 8 deletions(-) diff --git a/ProvisionQL/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index 809bf3f..b6d7788 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -621,14 +621,11 @@ OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, // App Info NSDictionary *plistApp = readPlistApp(meta); - ALLOW_EXIT - [infoLayer addEntriesFromDictionary:procAppInfo(plistApp)]; ALLOW_EXIT // Provisioning NSDictionary *plistProvision = readPlistProvision(meta); - ALLOW_EXIT if (!plistApp && !plistProvision) { return noErr; // nothing to do. Maybe another QL plugin can do better. @@ -644,14 +641,11 @@ OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, // Entitlements NSString *bundleExecutable = plistApp[@"CFBundleExecutable"]; NSData *codeSignData = getCodeSignEntitlements(meta, bundleExecutable); - ALLOW_EXIT - [infoLayer addEntriesFromDictionary:procEntitlements(codeSignData, plistProvision)]; ALLOW_EXIT // File Info [infoLayer addEntriesFromDictionary:procFileInfo(meta.url)]; - ALLOW_EXIT // Footer Info [infoLayer addEntriesFromDictionary:procFooterInfo()]; @@ -659,7 +653,6 @@ OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, // prepare html, replace values NSString *html = applyHtmlTemplate(infoLayer); - ALLOW_EXIT // QL render html NSDictionary *properties = @{ // properties for the HTML data diff --git a/ProvisionQL/GenerateThumbnailForURL.m b/ProvisionQL/GenerateThumbnailForURL.m index 8f6e20d..a316deb 100644 --- a/ProvisionQL/GenerateThumbnailForURL.m +++ b/ProvisionQL/GenerateThumbnailForURL.m @@ -117,7 +117,6 @@ OSStatus renderProvision(QuickLookInfo meta, QLThumbnailRequestRef thumbnail, BO NSSize badgeNumSize = [badge sizeWithAttributes:fontAttrs]; int badgeWidth = badgeNumSize.width + BADGE_MARGIN * 2; NSRect badgeOutlineRect = NSMakeRect(badgeX, badgeY, MAX(badgeWidth, MIN_BADGE_WIDTH), BADGE_HEIGHT); - ALLOW_EXIT // 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); From 22222e7697e20eb3025bdd077b4ac5383e46acf8 Mon Sep 17 00:00:00 2001 From: relikd Date: Thu, 25 Jan 2024 19:46:08 +0100 Subject: [PATCH 04/35] fix: disable downscaling for retina display --- ProvisionQL/GeneratePreviewForURL.m | 4 +++- ProvisionQL/GenerateThumbnailForURL.m | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ProvisionQL/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index b6d7788..871c1e4 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -603,7 +603,9 @@ unsigned long long getFileSize(NSString *path) { [rv appendString:[html substringWithRange:NSMakeRange(prevLoc, start - prevLoc)]]; NSString *value = templateValues[key]; if (!value) { - NSLog(@"WARN: unused key %@", key); +#ifdef DEBUG + NSLog(@"WARN: unused key %@", key); +#endif } else { [rv appendString:value]; } diff --git a/ProvisionQL/GenerateThumbnailForURL.m b/ProvisionQL/GenerateThumbnailForURL.m index a316deb..f8963bd 100644 --- a/ProvisionQL/GenerateThumbnailForURL.m +++ b/ProvisionQL/GenerateThumbnailForURL.m @@ -33,11 +33,11 @@ OSStatus renderAppIcon(QuickLookInfo meta, QLThumbnailRequestRef thumbnail) { NSImage *appIcon = imageFromApp(meta, nil); ALLOW_EXIT - // downscale as required by QLThumbnailRequestSetImageWithData - CGSize maxSize = QLThumbnailRequestGetMaximumSize(thumbnail); - if (appIcon.size.width > maxSize.width && appIcon.size.height > maxSize.height) { - [appIcon setSize:maxSize]; - } + // if downscale, then this should respect retina resolution +// CGSize maxSize = QLThumbnailRequestGetMaximumSize(thumbnail); +// if (appIcon.size.width > maxSize.width && appIcon.size.height > maxSize.height) { +// [appIcon setSize:maxSize]; +// } appIcon = roundCorners(appIcon); ALLOW_EXIT From 7654784523ea8026838e5b2bb486e408fb2af2f9 Mon Sep 17 00:00:00 2001 From: relikd Date: Thu, 25 Jan 2024 20:04:08 +0100 Subject: [PATCH 05/35] feat: inline helper functions --- ProvisionQL/Shared.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ProvisionQL/Shared.m b/ProvisionQL/Shared.m index 7eaa4c8..d8fd7d8 100644 --- a/ProvisionQL/Shared.m +++ b/ProvisionQL/Shared.m @@ -135,12 +135,12 @@ ExpirationStatus expirationStatus(NSDate *date) { } /// Ensures the value is of type @c NSDate -NSDate * _Nullable dateOrNil(NSDate * _Nullable value) { +inline NSDate * _Nullable dateOrNil(NSDate * _Nullable value) { return [value isKindOfClass:[NSDate class]] ? value : nil; } /// Ensures the value is of type @c NSArray -NSArray * _Nullable arrayOrNil(NSArray * _Nullable value) { +inline NSArray * _Nullable arrayOrNil(NSArray * _Nullable value) { return [value isKindOfClass:[NSArray class]] ? value : nil; } From 8ef12cd14825df59dd565b2bba89081e6558433c Mon Sep 17 00:00:00 2001 From: relikd Date: Sun, 28 Jan 2024 21:36:28 +0100 Subject: [PATCH 06/35] feat: use zlib to unzip from ipa --- ProvisionQL.xcodeproj/project.pbxproj | 44 ++++ ProvisionQL/3rd-party/pinch/ZipEntry.h | 64 +++++ ProvisionQL/3rd-party/pinch/ZipEntry.m | 56 +++++ ProvisionQL/3rd-party/pinch/pinch.h | 35 +++ ProvisionQL/3rd-party/pinch/pinch.m | 329 +++++++++++++++++++++++++ ProvisionQL/GeneratePreviewForURL.m | 6 +- ProvisionQL/Shared.h | 7 +- ProvisionQL/Shared.m | 43 +--- ProvisionQL/ZipFile.h | 18 ++ ProvisionQL/ZipFile.m | 102 ++++++++ 10 files changed, 665 insertions(+), 39 deletions(-) create mode 100755 ProvisionQL/3rd-party/pinch/ZipEntry.h create mode 100755 ProvisionQL/3rd-party/pinch/ZipEntry.m create mode 100755 ProvisionQL/3rd-party/pinch/pinch.h create mode 100755 ProvisionQL/3rd-party/pinch/pinch.m create mode 100644 ProvisionQL/ZipFile.h create mode 100644 ProvisionQL/ZipFile.m diff --git a/ProvisionQL.xcodeproj/project.pbxproj b/ProvisionQL.xcodeproj/project.pbxproj index 3c5e9ef..344f721 100644 --- a/ProvisionQL.xcodeproj/project.pbxproj +++ b/ProvisionQL.xcodeproj/project.pbxproj @@ -7,6 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 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 */; }; 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 */; }; @@ -27,6 +34,13 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 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 = ""; }; 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 = ""; }; @@ -62,6 +76,7 @@ 55DB729B186E195500CAFEE7 /* Security.framework in Frameworks */, 55DB7287186E193500CAFEE7 /* CoreFoundation.framework in Frameworks */, 55DB7281186E193500CAFEE7 /* QuickLook.framework in Frameworks */, + 54F4EB0F2B668F7E0000CE41 /* libz.dylib in Frameworks */, 55DB7285186E193500CAFEE7 /* CoreServices.framework in Frameworks */, 55DB7283186E193500CAFEE7 /* ApplicationServices.framework in Frameworks */, ); @@ -70,6 +85,25 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 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 = ""; + }; 55424C641870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect */ = { isa = PBXGroup; children = ( @@ -138,6 +172,7 @@ 55DB727F186E193500CAFEE7 /* Frameworks */ = { isa = PBXGroup; children = ( + 54F4EB0E2B668F7E0000CE41 /* libz.dylib */, 557C842118731FB7008A2A0C /* WebKit.framework */, 55424C601870D4AA002F5408 /* AppKit.framework */, 55DB729A186E195500CAFEE7 /* Security.framework */, @@ -152,11 +187,14 @@ 55DB7288186E193500CAFEE7 /* ProvisionQL */ = { isa = PBXGroup; children = ( + 54F4EAFB2B6668940000CE41 /* 3rd-party */, 555E9511186E2D67001D406A /* Supporting-files */, 555E951A186E2DC0001D406A /* Scripts */, 555E9518186E2DC0001D406A /* Resources */, 557C842718733828008A2A0C /* Shared.h */, 557C842418732599008A2A0C /* Shared.m */, + 54F4EB202B66D6FE0000CE41 /* ZipFile.h */, + 54F4EB212B66D6FE0000CE41 /* ZipFile.m */, 55DB728E186E193500CAFEE7 /* GenerateThumbnailForURL.m */, 55DB7290186E193500CAFEE7 /* GeneratePreviewForURL.m */, ); @@ -171,7 +209,10 @@ buildActionMask = 2147483647; files = ( 55424C671870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.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; }; @@ -279,9 +320,12 @@ files = ( 55424C681870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.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 */, 55DB7291186E193500CAFEE7 /* GeneratePreviewForURL.m in Sources */, + 54F4EB232B66D6FE0000CE41 /* ZipFile.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 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/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index 871c1e4..a9236dc 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -436,6 +436,10 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla } [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]; @@ -457,7 +461,7 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla case FileTypeIPA: basePath = currentTempDirFolder; [fileManager createDirectoryAtPath:currentTempDirFolder withIntermediateDirectories:YES attributes:nil error:nil]; - unzipFileToDir(meta.url, currentTempDirFolder, [@"Payload/*.app/" stringByAppendingPathComponent:bundleExecutable]); + [meta.zipFile unzipFile:[@"Payload/*.app/" stringByAppendingPathComponent:bundleExecutable] toDir:currentTempDirFolder]; break; case FileTypeArchive: basePath = meta.effectiveUrl.path; diff --git a/ProvisionQL/Shared.h b/ProvisionQL/Shared.h index a39ba90..87d8741 100644 --- a/ProvisionQL/Shared.h +++ b/ProvisionQL/Shared.h @@ -8,6 +8,8 @@ #import +#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"; @@ -31,14 +33,11 @@ typedef struct QuickLookMeta { FileType type; BOOL isOSX; + ZipFile * _Nullable zipFile; // only set for zipped file types } QuickLookInfo; QuickLookInfo initQLInfo(_Nonnull CFStringRef contentTypeUTI, _Nonnull CFURLRef url); - -// Unzip -void unzipFileToDir(NSURL * _Nonnull url, NSString * _Nonnull filePath, NSString * _Nonnull targetDir); - // Plist NSDictionary * _Nullable readPlistApp(QuickLookInfo meta); NSDictionary * _Nullable readPlistProvision(QuickLookInfo meta); diff --git a/ProvisionQL/Shared.m b/ProvisionQL/Shared.m index d8fd7d8..834d2af 100644 --- a/ProvisionQL/Shared.m +++ b/ProvisionQL/Shared.m @@ -1,4 +1,5 @@ #import "Shared.h" +#import "ZipFile.h" // MARK: - Meta data for QuickLook @@ -22,6 +23,7 @@ QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { if ([data.UTI isEqualToString:kDataType_ipa]) { 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); @@ -38,50 +40,20 @@ QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { return data; } - -// MARK: Unzip - -/// Unzip file directly into memory. -NSData * _Nullable unzipFile(NSURL *url, NSString *filePath) { - NSTask *task = [NSTask new]; - [task setLaunchPath:@"/usr/bin/unzip"]; - [task setStandardOutput:[NSPipe pipe]]; - [task setArguments:@[@"-p", [url path], filePath]]; // @"-x", @"*/*/*/*" - [task launch]; - - NSData *pipeData = [[[task standardOutput] fileHandleForReading] readDataToEndOfFile]; - [task waitUntilExit]; - if (pipeData.length == 0) { - return nil; - } - return pipeData; -} - -/// Unzip file to filesystem. -void unzipFileToDir(NSURL *url, NSString *targetDir, NSString *filePath) { - NSTask *task = [NSTask new]; - [task setLaunchPath:@"/usr/bin/unzip"]; - [task setArguments:@[@"-u", @"-j", @"-d", targetDir, [url path], filePath]]; // @"-x", @"*/*/*/*" - [task launch]; - [task waitUntilExit]; -} - /// 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 unzipFile(meta.url, [@"Payload/*.app/" stringByAppendingString:filename]); + 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; } } - -// MARK: Plist +// MARK: - Plist /// Read app default @c Info.plist. NSDictionary * _Nullable readPlistApp(QuickLookInfo meta) { - NSLog(@"read once"); switch (meta.type) { case FileTypeIPA: case FileTypeArchive: @@ -217,10 +189,13 @@ ExpirationStatus expirationStatus(NSDate *date) { /// @param appPlist If @c nil, will load plist on the fly (used for thumbnail) NSImage * _Nonnull imageFromApp(QuickLookInfo meta, NSDictionary *appPlist) { if (meta.type == FileTypeIPA) { - NSData *data = unzipFile(meta.url, @"iTunesArtwork"); + NSData *data = [meta.zipFile unzipFile:@"iTunesArtwork"]; if (!data) { NSString *fileName = mainIconNameForApp(appPlist ?: readPlistApp(meta)); - data = unzipFile(meta.url, [NSString stringWithFormat:@"Payload/*.app/%@*", fileName]); + if (fileName) { + data = [meta.zipFile unzipFile:[NSString stringWithFormat:@"Payload/*.app/%@*", fileName]]; + } + // TODO: load assets.car } if (data) { return [[NSImage alloc] initWithData:data]; diff --git a/ProvisionQL/ZipFile.h b/ProvisionQL/ZipFile.h new file mode 100644 index 0000000..d2885aa --- /dev/null +++ b/ProvisionQL/ZipFile.h @@ -0,0 +1,18 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ZipFile : NSObject ++ (instancetype)open:(NSString *)path; + +/// Unzip file directly into memory. +/// @param filePath File path inside zip file. +- (NSData * _Nullable)unzipFile:(NSString *)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; +@end + +NS_ASSUME_NONNULL_END diff --git a/ProvisionQL/ZipFile.m b/ProvisionQL/ZipFile.m new file mode 100644 index 0000000..9d444e3 --- /dev/null +++ b/ProvisionQL/ZipFile.m @@ -0,0 +1,102 @@ +#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 + +- (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]; +} + +- (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 From 33f9062cae6e559028e21ffd62f14b1c503b3443 Mon Sep 17 00:00:00 2001 From: relikd Date: Sun, 28 Jan 2024 21:54:48 +0100 Subject: [PATCH 07/35] chore: update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4f2704..d22537d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # ProvisionQL +## Version 1.7.0 + +* 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+ +* unzip with zlib instead of sys-call (performance) +* parse html template tags with regex (performance) +* use higher resolution app icon if available (try `iTunesArtwork`) +* 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)) From b37bb6f2781f69a79cbc8c1a89ca78158f8abb8e Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 29 Jan 2024 17:32:04 +0100 Subject: [PATCH 08/35] fix: dont load plist if data is nil --- ProvisionQL/Shared.m | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ProvisionQL/Shared.m b/ProvisionQL/Shared.m index 834d2af..cb4b7d8 100644 --- a/ProvisionQL/Shared.m +++ b/ProvisionQL/Shared.m @@ -52,14 +52,19 @@ QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { // MARK: - Plist +/// Helper for optional chaining. +NSDictionary * _Nullable asPlistOrNil(NSData * _Nullable data) { + if (!data) { return nil; } + return [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL]; +} + /// Read app default @c Info.plist. NSDictionary * _Nullable readPlistApp(QuickLookInfo meta) { switch (meta.type) { case FileTypeIPA: case FileTypeArchive: case FileTypeExtension: { - NSData *plistData = readPayloadFile(meta, @"Info.plist"); - return [NSPropertyListSerialization propertyListWithData:plistData options:0 format:NULL error:NULL]; + return asPlistOrNil(readPayloadFile(meta, @"Info.plist")); } case FileTypeProvision: return nil; @@ -87,11 +92,7 @@ QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { CMSDecoderCopyContent(decoder, &dataRef); NSData *data = (NSData *)CFBridgingRelease(dataRef); CFRelease(decoder); - - if (!data) { - return nil; - } - return [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL]; + return asPlistOrNil(data); } From 5ec9661dbe1797a025a4390f4543922a788296b1 Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 29 Jan 2024 17:34:08 +0100 Subject: [PATCH 09/35] ref: date-string parser --- ProvisionQL/GeneratePreviewForURL.m | 34 ++++++++++++++++++----------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/ProvisionQL/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index a9236dc..a6a4d35 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -146,6 +146,25 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla 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) { + NSLog(@"ERROR formatting date: %@", dateStr); + } + return rv; +} + /// @return Relative distance to today. E.g., "Expired today" NSString * _Nullable relativeExpirationDateString(NSDate *date) { if (!date) { @@ -295,14 +314,7 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla // 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) { - if ([value isKindOfClass:[NSDate class]]) { - invalidityDate = value; - } else { - // parse the date from a string - NSDateFormatter *dateFormatter = [NSDateFormatter new]; - [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"]; - invalidityDate = [dateFormatter dateFromString:[value description]]; - } + invalidityDate = parseDate(value); } else { NSLog(@"No invalidity date in '%@' certificate, dictionary = %@", subject, innerDictRef); } @@ -606,11 +618,7 @@ unsigned long long getFileSize(NSString *path) { 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) { -#ifdef DEBUG - NSLog(@"WARN: unused key %@", key); -#endif - } else { + if (value) { [rv appendString:value]; } prevLoc = start + result.range.length; From 96495d9db391511d9ea8bc6cee507fea29a4a3ec Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 29 Jan 2024 17:38:42 +0100 Subject: [PATCH 10/35] feat: show iTunes Metadata + purchase information --- CHANGELOG.md | 19 ++-- ProvisionQL.xcodeproj/project.pbxproj | 8 ++ ProvisionQL/AppCategories.h | 4 + ProvisionQL/AppCategories.m | 142 ++++++++++++++++++++++++++ ProvisionQL/GeneratePreviewForURL.m | 66 ++++++++++++ ProvisionQL/Resources/template.html | 13 +++ ProvisionQL/Shared.h | 1 + ProvisionQL/Shared.m | 7 ++ 8 files changed, 251 insertions(+), 9 deletions(-) create mode 100644 ProvisionQL/AppCategories.h create mode 100644 ProvisionQL/AppCategories.m diff --git a/CHANGELOG.md b/CHANGELOG.md index d22537d..722220a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,16 @@ ## Version 1.7.0 -* 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+ -* unzip with zlib instead of sys-call (performance) -* parse html template tags with regex (performance) -* use higher resolution app icon if available (try `iTunesArtwork`) -* minor html template improvements -* some refactoring to reduce duplicate code +* New: show iTunes Metadata & purchase information +* New: use higher resolution app icon if available (try `iTunesArtwork`) +* Performance: unzip with zlib instead of sys-call +* Performance: parse html template tags with regex +* 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+ +* Minor html template improvements +* Some refactoring to reduce duplicate code ## Version 1.6.4 diff --git a/ProvisionQL.xcodeproj/project.pbxproj b/ProvisionQL.xcodeproj/project.pbxproj index 344f721..f529809 100644 --- a/ProvisionQL.xcodeproj/project.pbxproj +++ b/ProvisionQL.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 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 */; }; @@ -41,6 +43,8 @@ 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 = ""; }; @@ -191,6 +195,8 @@ 555E9511186E2D67001D406A /* Supporting-files */, 555E951A186E2DC0001D406A /* Scripts */, 555E9518186E2DC0001D406A /* Resources */, + 54F4EB662B6719310000CE41 /* AppCategories.h */, + 54F4EB672B6719310000CE41 /* AppCategories.m */, 557C842718733828008A2A0C /* Shared.h */, 557C842418732599008A2A0C /* Shared.m */, 54F4EB202B66D6FE0000CE41 /* ZipFile.h */, @@ -208,6 +214,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 54F4EB682B6719310000CE41 /* AppCategories.h in Headers */, 55424C671870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.h in Headers */, 54F4EB002B6668A50000CE41 /* ZipEntry.h in Headers */, 557C842818733828008A2A0C /* Shared.h in Headers */, @@ -324,6 +331,7 @@ 54F4EB022B6668A50000CE41 /* pinch.m in Sources */, 557C842618732599008A2A0C /* Shared.m in Sources */, 55DB728F186E193500CAFEE7 /* GenerateThumbnailForURL.m in Sources */, + 54F4EB692B6719310000CE41 /* AppCategories.m in Sources */, 55DB7291186E193500CAFEE7 /* GeneratePreviewForURL.m in Sources */, 54F4EB232B66D6FE0000CE41 /* ZipFile.m in Sources */, ); 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..489d00c --- /dev/null +++ b/ProvisionQL/AppCategories.m @@ -0,0 +1,142 @@ +#import "AppCategories.h" + +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/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index a6a4d35..0207fc8 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -1,4 +1,5 @@ #import "Shared.h" +#import "AppCategories.h" // makro to stop further processing #define ALLOW_EXIT if (QLPreviewRequestIsCancelled(preview)) { return noErr; } @@ -159,6 +160,10 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla 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); } @@ -299,6 +304,63 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla } +// 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. @@ -638,6 +700,10 @@ OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, [infoLayer addEntriesFromDictionary:procAppInfo(plistApp)]; ALLOW_EXIT + NSDictionary *plistItunes = readPlistItunes(meta); + [infoLayer addEntriesFromDictionary:parseItunesMeta(plistItunes)]; + ALLOW_EXIT + // Provisioning NSDictionary *plistProvision = readPlistProvision(meta); diff --git a/ProvisionQL/Resources/template.html b/ProvisionQL/Resources/template.html index 2d04525..8fb91e7 100644 --- a/ProvisionQL/Resources/template.html +++ b/ProvisionQL/Resources/template.html @@ -185,6 +185,19 @@

Devices (__ProvisionedDevicesCount__)

__ProvisionedDevicesFormatted__
+ +
+

iTunes Metadata

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

File info

__FileName__
diff --git a/ProvisionQL/Shared.h b/ProvisionQL/Shared.h index 87d8741..6e572cf 100644 --- a/ProvisionQL/Shared.h +++ b/ProvisionQL/Shared.h @@ -41,6 +41,7 @@ QuickLookInfo initQLInfo(_Nonnull CFStringRef contentTypeUTI, _Nonnull CFURLRef // Plist NSDictionary * _Nullable readPlistApp(QuickLookInfo meta); NSDictionary * _Nullable readPlistProvision(QuickLookInfo meta); +NSDictionary * _Nullable readPlistItunes(QuickLookInfo meta); // Other helper typedef NS_ENUM(NSUInteger, ExpirationStatus) { diff --git a/ProvisionQL/Shared.m b/ProvisionQL/Shared.m index cb4b7d8..e6a9097 100644 --- a/ProvisionQL/Shared.m +++ b/ProvisionQL/Shared.m @@ -95,6 +95,13 @@ QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { return asPlistOrNil(data); } +/// Read @c iTunesMetadata.plist if available +NSDictionary * _Nullable readPlistItunes(QuickLookInfo meta) { + if (meta.type == FileTypeIPA) { + return asPlistOrNil([meta.zipFile unzipFile:@"iTunesMetadata.plist"]); + } + return nil; +} // MARK: - Other helper From dc111a18e8a4ed02db0fe99dabb7a9579cf0ae85 Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 29 Jan 2024 18:04:51 +0100 Subject: [PATCH 11/35] fix: remove potential leak --- ProvisionQL/GeneratePreviewForURL.m | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ProvisionQL/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index 0207fc8..c55e561 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -267,10 +267,6 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla }; } - NSString *bundleName = appPlist[@"CFBundleDisplayName"]; - if (!bundleName) { - bundleName = appPlist[@"CFBundleName"]; - } NSString *extensionType = appPlist[@"NSExtension"][@"NSExtensionPointIdentifier"]; NSMutableArray *platforms = [NSMutableArray array]; @@ -369,7 +365,7 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla NSDate *invalidityDate = nil; CFErrorRef error = nil; CFDictionaryRef outerDictRef = SecCertificateCopyValues(certificateRef, (__bridge CFArrayRef)@[(__bridge NSString*)kSecOIDInvalidityDate], &error); - if (outerDictRef && !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". From 4ed661439fdb4bdea6644013aa199d0b1ef9825b Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 29 Jan 2024 21:50:46 +0100 Subject: [PATCH 12/35] chore: update settings + version bump --- ProvisionQL.xcodeproj/project.pbxproj | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ProvisionQL.xcodeproj/project.pbxproj b/ProvisionQL.xcodeproj/project.pbxproj index f529809..8868d1b 100644 --- a/ProvisionQL.xcodeproj/project.pbxproj +++ b/ProvisionQL.xcodeproj/project.pbxproj @@ -252,7 +252,7 @@ 55DB7273186E193500CAFEE7 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1240; ORGANIZATIONNAME = "Evgeny Aleksandrov"; TargetAttributes = { 55DB727C186E193500CAFEE7 = { @@ -361,6 +361,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; @@ -412,6 +413,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; @@ -447,7 +449,7 @@ ENABLE_HARDENED_RUNTIME = YES; 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)"; @@ -467,7 +469,7 @@ ENABLE_HARDENED_RUNTIME = YES; 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)"; From da872e7cca279863167881afdd63b74ee42f7ddc Mon Sep 17 00:00:00 2001 From: relikd Date: Tue, 30 Jan 2024 23:18:33 +0100 Subject: [PATCH 13/35] ref: rename ProvisionHidden and split div --- ProvisionQL/GeneratePreviewForURL.m | 4 ++-- ProvisionQL/Resources/template.html | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/ProvisionQL/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index c55e561..fa57699 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -462,7 +462,7 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla NSDictionary * _Nonnull procProvision(NSDictionary *provisionPlist, BOOL isOSX) { if (!provisionPlist) { return @{ - @"ProvisionInfo": @"hiddenDiv", + @"ProvisionHidden": @"hiddenDiv", }; } @@ -471,7 +471,7 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla NSArray* devices = getDeviceList(provisionPlist); return @{ - @"ProvisionInfo": @"", + @"ProvisionHidden": @"", @"ProfileName": provisionPlist[@"Name"] ?: @"", @"ProfileUUID": provisionPlist[@"UUID"] ?: @"", @"TeamName": provisionPlist[@"TeamName"] ?: @"Team name not available", diff --git a/ProvisionQL/Resources/template.html b/ProvisionQL/Resources/template.html index 8fb91e7..fcbea7b 100644 --- a/ProvisionQL/Resources/template.html +++ b/ProvisionQL/Resources/template.html @@ -157,7 +157,7 @@

App Transport Security

__AppTransportSecurityFormatted__
-
+

Provisioning

Profile name: __ProfileName__
@@ -171,19 +171,24 @@

__ProfileName__

Team: __TeamName__ (__TeamIds__)
Creation date: __CreationDateFormatted__
Expiration Date: __ExpirationDateFormatted__
+
-

Entitlements

-
- Entitlements extraction failed. -
- __EntitlementsFormatted__ +
+

Entitlements

+
+ Entitlements extraction failed. +
+ __EntitlementsFormatted__ +
+

Developer Certificates

__DeveloperCertificatesFormatted__ +
+

Devices (__ProvisionedDevicesCount__)

- __ProvisionedDevicesFormatted__ - + __ProvisionedDevicesFormatted__
From 8a96cdf995c59ddc0b688651daf43e849cc93667 Mon Sep 17 00:00:00 2001 From: relikd Date: Tue, 30 Jan 2024 23:24:23 +0100 Subject: [PATCH 14/35] feat: replace codesign sys-call with SecCodeSigning --- CHANGELOG.md | 1 + ProvisionQL.xcodeproj/project.pbxproj | 8 + ProvisionQL/Entitlements.h | 17 +++ ProvisionQL/Entitlements.m | 212 ++++++++++++++++++++++++++ ProvisionQL/GeneratePreviewForURL.m | 140 +++-------------- 5 files changed, 259 insertions(+), 119 deletions(-) create mode 100644 ProvisionQL/Entitlements.h create mode 100644 ProvisionQL/Entitlements.m diff --git a/CHANGELOG.md b/CHANGELOG.md index 722220a..922e655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * New: use higher resolution app icon if available (try `iTunesArtwork`) * 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) diff --git a/ProvisionQL.xcodeproj/project.pbxproj b/ProvisionQL.xcodeproj/project.pbxproj index 8868d1b..b600748 100644 --- a/ProvisionQL.xcodeproj/project.pbxproj +++ b/ProvisionQL.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 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 */; }; @@ -36,6 +38,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 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 = ""; }; @@ -197,6 +201,8 @@ 555E9518186E2DC0001D406A /* Resources */, 54F4EB662B6719310000CE41 /* AppCategories.h */, 54F4EB672B6719310000CE41 /* AppCategories.m */, + 54B1E0092B6989E7009E654A /* Entitlements.h */, + 54B1E00A2B6989E7009E654A /* Entitlements.m */, 557C842718733828008A2A0C /* Shared.h */, 557C842418732599008A2A0C /* Shared.m */, 54F4EB202B66D6FE0000CE41 /* ZipFile.h */, @@ -216,6 +222,7 @@ files = ( 54F4EB682B6719310000CE41 /* AppCategories.h in Headers */, 55424C671870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.h in Headers */, + 54B1E00B2B6989E7009E654A /* Entitlements.h in Headers */, 54F4EB002B6668A50000CE41 /* ZipEntry.h in Headers */, 557C842818733828008A2A0C /* Shared.h in Headers */, 54F4EB032B6668A50000CE41 /* pinch.h in Headers */, @@ -332,6 +339,7 @@ 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 */, ); 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 fa57699..6ceedfc 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -1,5 +1,6 @@ #import "Shared.h" #import "AppCategories.h" +#import "Entitlements.h" // makro to stop further processing #define ALLOW_EXIT if (QLPreviewRequestIsCancelled(preview)) { return noErr; } @@ -32,48 +33,6 @@ return table; } -/// 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]; - } - } -} - /// Print recursive tree of key-value mappings. void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *replacements, int level, NSMutableString *output) { for (NSString *key in dictionary) { @@ -493,45 +452,20 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla // MARK: - Entitlements -/// run: @c codesign -d --entitlements - --xml -NSData *runCodeSign(NSString *binaryPath) { - 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.length == 0) { - return errorData; - } - return outputData; -} - /// Search for app binary and run @c codesign on it. -NSData *getCodeSignEntitlements(QuickLookInfo meta, NSString *bundleExecutable) { - NSFileManager *fileManager = [NSFileManager defaultManager]; +Entitlements *readEntitlements(QuickLookInfo meta, NSString *bundleExecutable) { + if (!bundleExecutable) { + return [Entitlements withoutBinary]; + } NSString *tempDirFolder = [NSTemporaryDirectory() stringByAppendingPathComponent:kPluginBundleId]; - NSString *currentTempDirFolder = [tempDirFolder stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; - + NSString *currentTempDirFolder = nil; NSString *basePath = nil; switch (meta.type) { case FileTypeIPA: - basePath = currentTempDirFolder; - [fileManager createDirectoryAtPath:currentTempDirFolder withIntermediateDirectories:YES attributes:nil error:nil]; + 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; @@ -543,51 +477,21 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla return nil; } - NSData *data = runCodeSign([basePath stringByAppendingPathComponent:bundleExecutable]); - [fileManager removeItemAtPath:currentTempDirFolder error:nil]; - return data; -} - -/// Print formatted plist in a @c \
 tag
-NSString * _Nonnull formattedPlist(NSDictionary *dict) {
-	NSMutableString *output = [NSMutableString string];
-	recursiveKeyValue(0, nil, dict, output);
-	return [NSString stringWithFormat:@"
%@
", output]; + Entitlements *rv = [Entitlements withBinary:[basePath stringByAppendingPathComponent:bundleExecutable]]; + if (currentTempDirFolder) { + [[NSFileManager defaultManager] removeItemAtPath:currentTempDirFolder error:nil]; + } + return rv; } -/// First, try to extract real entitlements by running codesign. -/// If that fails, fallback to entitlements provided by provision plist. -NSDictionary * _Nonnull procEntitlements(NSData *codeSignData, NSDictionary *provisionPlist) { - BOOL showEntitlementsWarning = false; - NSString *formattedOutput = nil; - if (codeSignData != nil) { - NSDictionary *plist = [NSPropertyListSerialization propertyListWithData:codeSignData options:0 format:NULL error:NULL]; - if (plist != nil) { - formattedOutput = formattedPlist(plist); - } else { - showEntitlementsWarning = true; - NSString *output = [[NSString alloc] initWithData:codeSignData encoding:NSUTF8StringEncoding]; - if ([output hasPrefix:@"Executable="]) { - // remove first line with long temporary path to the executable - NSArray *allLines = [output componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; - formattedOutput = [[allLines subarrayWithRange:NSMakeRange(1, allLines.count - 1)] componentsJoinedByString:@"
"]; - } else { - formattedOutput = output; - } - } - } else { - // read the entitlements from the provisioning profile instead - NSDictionary *value = provisionPlist[@"Entitlements"]; - if ([value isKindOfClass:[NSDictionary class]]) { - formattedOutput = formattedPlist(value); - } else { - formattedOutput = @"No Entitlements"; - } - } +/// 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 @{ - @"EntitlementsFormatted": formattedOutput ?: @"", - @"EntitlementsWarning": showEntitlementsWarning ? @"" : @"hiddenDiv", + @"EntitlementsFormatted": entitlements.html ?: @"No Entitlements", + @"EntitlementsWarning": entitlements.hasError ? @"" : @"hiddenDiv", }; } @@ -715,9 +619,7 @@ OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, ALLOW_EXIT // Entitlements - NSString *bundleExecutable = plistApp[@"CFBundleExecutable"]; - NSData *codeSignData = getCodeSignEntitlements(meta, bundleExecutable); - [infoLayer addEntriesFromDictionary:procEntitlements(codeSignData, plistProvision)]; + [infoLayer addEntriesFromDictionary:procEntitlements(meta, plistApp, plistProvision)]; ALLOW_EXIT // File Info From 61b7cf28f4a650d084aa126b80afbfcd13b3481c Mon Sep 17 00:00:00 2001 From: relikd Date: Tue, 30 Jan 2024 23:31:13 +0100 Subject: [PATCH 15/35] style: remove css indentation --- ProvisionQL/Resources/template.html | 344 ++++++++++++++-------------- 1 file changed, 172 insertions(+), 172 deletions(-) diff --git a/ProvisionQL/Resources/template.html b/ProvisionQL/Resources/template.html index fcbea7b..aa3c5cf 100644 --- a/ProvisionQL/Resources/template.html +++ b/ProvisionQL/Resources/template.html @@ -1,172 +1,172 @@ - - - - - - -
-

__AppInfoTitle__

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

__AppInfoTitle__

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

App Transport Security

- __AppTransportSecurityFormatted__ -
- -
-
-

Provisioning

- Profile name: __ProfileName__
-
-
-

__ProfileName__

-
- - Profile UUID: __ProfileUUID__
+ 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__
@@ -203,13 +203,13 @@

iTunes Metadata

Price: __iTunesPrice__
-
-

File info

- __FileName__
- __FileInfo__
-
- - +
+

File info

+ __FileName__
+ __FileInfo__
+
+ + From 7e61bc419568bd809f130e4c09e6e4197523d643 Mon Sep 17 00:00:00 2001 From: relikd Date: Tue, 30 Jan 2024 23:48:21 +0100 Subject: [PATCH 16/35] ref: naming convention for all hiddenDiv --- ProvisionQL/GeneratePreviewForURL.m | 14 +++++++------- ProvisionQL/Resources/template.html | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ProvisionQL/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index 6ceedfc..9b7afa4 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -221,8 +221,8 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla NSDictionary * _Nonnull procAppInfo(NSDictionary *appPlist) { if (!appPlist) { return @{ - @"AppInfo": @"hiddenDiv", - @"ProvisionAsSubheader": @"", + @"AppInfoHidden": @"hiddenDiv", + @"ProvisionTitleHidden": @"", }; } @@ -240,16 +240,16 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla } return @{ - @"AppInfo": @"", - @"ProvisionAsSubheader": @"hiddenDiv", + @"AppInfoHidden": @"", + @"ProvisionTitleHidden": @"hiddenDiv", @"CFBundleName": appPlist[@"CFBundleDisplayName"] ?: appPlist[@"CFBundleName"] ?: @"", @"CFBundleShortVersionString": appPlist[@"CFBundleShortVersionString"] ?: @"", @"CFBundleVersion": appPlist[@"CFBundleVersion"] ?: @"", @"CFBundleIdentifier": appPlist[@"CFBundleIdentifier"] ?: @"", - @"ExtensionInfo": extensionType ? @"" : @"hiddenDiv", - @"NSExtensionPointIdentifier": extensionType ?: @"", + @"ExtensionTypeHidden": extensionType ? @"" : @"hiddenDiv", + @"ExtensionType": extensionType ?: @"", @"UIDeviceFamily": [platforms componentsJoinedByString:@", "], @"DTSDKName": appPlist[@"DTSDKName"] ?: @"", @@ -490,8 +490,8 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla [entitlements applyFallbackIfNeeded:provisionPlist[@"Entitlements"]]; return @{ + @"EntitlementsWarningHidden": entitlements.hasError ? @"" : @"hiddenDiv", @"EntitlementsFormatted": entitlements.html ?: @"No Entitlements", - @"EntitlementsWarning": entitlements.hasError ? @"" : @"hiddenDiv", }; } diff --git a/ProvisionQL/Resources/template.html b/ProvisionQL/Resources/template.html index aa3c5cf..8e02dda 100644 --- a/ProvisionQL/Resources/template.html +++ b/ProvisionQL/Resources/template.html @@ -138,15 +138,15 @@ -
+

__AppInfoTitle__

App icon
Name: __CFBundleName__
Version: __CFBundleShortVersionString__ (__CFBundleVersion__)
BundleId: __CFBundleIdentifier__
-
- Extension type: __NSExtensionPointIdentifier__
+
+ Extension type: __ExtensionType__
DeviceFamily: __UIDeviceFamily__
SDK: __DTSDKName__
@@ -158,11 +158,11 @@

App Transport Security

-
+

Provisioning

Profile name: __ProfileName__
-
+

__ProfileName__

@@ -175,7 +175,7 @@

__ProfileName__

Entitlements

-
+
Entitlements extraction failed.
__EntitlementsFormatted__ From d46017ebc735157f80da212c349fd6eadeb4f94a Mon Sep 17 00:00:00 2001 From: relikd Date: Tue, 30 Jan 2024 23:56:20 +0100 Subject: [PATCH 17/35] doc: py script for app categories --- CHANGELOG.md | 1 + ProvisionQL/AppCategories.m | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 922e655..86830f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * 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 * Performance: unzip with zlib instead of sys-call * Performance: parse html template tags with regex * Performance: use `SecCodeSigning` instead of `codesign` sys-call diff --git a/ProvisionQL/AppCategories.m b/ProvisionQL/AppCategories.m index 489d00c..80508da 100644 --- a/ProvisionQL/AppCategories.m +++ b/ProvisionQL/AppCategories.m @@ -1,5 +1,26 @@ #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; From 94c645438a3c0cdfadefec2e36be12196cb6a43b Mon Sep 17 00:00:00 2001 From: relikd Date: Sun, 4 Feb 2024 20:52:00 +0100 Subject: [PATCH 18/35] fix: default case return --- ProvisionQL/GeneratePreviewForURL.m | 4 ++++ ProvisionQL/GenerateThumbnailForURL.m | 2 +- ProvisionQL/Shared.m | 10 +++++++++- ProvisionQL/ZipFile.h | 7 ------- ProvisionQL/ZipFile.m | 5 +++++ 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/ProvisionQL/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index 9b7afa4..99314f8 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -506,6 +506,7 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla case FileTypeExtension: return @"App extension info"; case FileTypeProvision: return nil; } + return nil; } /// Calculate file / folder size. @@ -592,6 +593,9 @@ unsigned long long getFileSize(NSString *path) { 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); diff --git a/ProvisionQL/GenerateThumbnailForURL.m b/ProvisionQL/GenerateThumbnailForURL.m index f8963bd..b20ad99 100644 --- a/ProvisionQL/GenerateThumbnailForURL.m +++ b/ProvisionQL/GenerateThumbnailForURL.m @@ -27,7 +27,7 @@ This function's job is to create thumbnail for designated file as fast as possible ----------------------------------------------------------------------------- */ -// MARK: .ipa .xarchive +// MARK: .ipa .xcarchive OSStatus renderAppIcon(QuickLookInfo meta, QLThumbnailRequestRef thumbnail) { NSImage *appIcon = imageFromApp(meta, nil); diff --git a/ProvisionQL/Shared.m b/ProvisionQL/Shared.m index e6a9097..5f6c63a 100644 --- a/ProvisionQL/Shared.m +++ b/ProvisionQL/Shared.m @@ -48,6 +48,7 @@ QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { case FileTypeExtension: return [NSData dataWithContentsOfURL:[meta.url URLByAppendingPathComponent:filename]]; case FileTypeProvision: return nil; } + return nil; } // MARK: - Plist @@ -55,7 +56,13 @@ QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { /// Helper for optional chaining. NSDictionary * _Nullable asPlistOrNil(NSData * _Nullable data) { if (!data) { return nil; } - return [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL]; + NSError *err; + NSDictionary *dict = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:&err]; + if (err) { + NSLog(@"ERROR reading plist %@", err); + return nil; + } + return dict; } /// Read app default @c Info.plist. @@ -69,6 +76,7 @@ QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { case FileTypeProvision: return nil; } + return nil; } /// Read @c embedded.mobileprovision file and decode with CMS decoder. diff --git a/ProvisionQL/ZipFile.h b/ProvisionQL/ZipFile.h index d2885aa..f87d33b 100644 --- a/ProvisionQL/ZipFile.h +++ b/ProvisionQL/ZipFile.h @@ -4,14 +4,7 @@ NS_ASSUME_NONNULL_BEGIN @interface ZipFile : NSObject + (instancetype)open:(NSString *)path; - -/// Unzip file directly into memory. -/// @param filePath File path inside zip file. - (NSData * _Nullable)unzipFile:(NSString *)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; @end diff --git a/ProvisionQL/ZipFile.m b/ProvisionQL/ZipFile.m index 9d444e3..9fe9d97 100644 --- a/ProvisionQL/ZipFile.m +++ b/ProvisionQL/ZipFile.m @@ -25,6 +25,8 @@ - (instancetype)initWithFile:(NSString *)path { // MARK: - public methods +/// 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]; @@ -49,6 +51,9 @@ - (NSData * _Nullable)unzipFile:(NSString *)filePath { 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]; From 9a76f08509daf78834dc080b3aeaff538c508a9e Mon Sep 17 00:00:00 2001 From: relikd Date: Sun, 4 Feb 2024 21:02:25 +0100 Subject: [PATCH 19/35] feat: support for Asset.car icons --- CHANGELOG.md | 2 + .../NSBezierPath+IOS7RoundedRect.h | 15 - .../NSBezierPath+IOS7RoundedRect.m | 49 --- PrivateFrameworks/CoreUI.framework/Headers | 1 + .../Versions/A/Headers/CUICatalog.h | 54 +++ .../Versions/A/Headers/CUINamedImage.h | 53 +++ .../Versions/A/Headers/CUINamedLookup.h | 16 + .../CoreUI.framework/Versions/Current | 1 + .../CoreUI.framework/module.modulemap | 9 + ProvisionQL.xcodeproj/project.pbxproj | 52 ++- ProvisionQL/AppIcon.h | 17 + ProvisionQL/AppIcon.m | 383 ++++++++++++++++++ ProvisionQL/GeneratePreviewForURL.m | 21 +- ProvisionQL/GenerateThumbnailForURL.m | 25 +- ProvisionQL/Shared.h | 7 +- ProvisionQL/Shared.m | 100 ----- ProvisionQL/ZipFile.h | 2 + ProvisionQL/ZipFile.m | 8 + 18 files changed, 602 insertions(+), 213 deletions(-) delete mode 100644 NSBezierPath+IOS7RoundedRect/NSBezierPath+IOS7RoundedRect.h delete mode 100644 NSBezierPath+IOS7RoundedRect/NSBezierPath+IOS7RoundedRect.m create mode 120000 PrivateFrameworks/CoreUI.framework/Headers create mode 100755 PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUICatalog.h create mode 100755 PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUINamedImage.h create mode 100755 PrivateFrameworks/CoreUI.framework/Versions/A/Headers/CUINamedLookup.h create mode 120000 PrivateFrameworks/CoreUI.framework/Versions/Current create mode 100755 PrivateFrameworks/CoreUI.framework/module.modulemap create mode 100644 ProvisionQL/AppIcon.h create mode 100644 ProvisionQL/AppIcon.m diff --git a/CHANGELOG.md b/CHANGELOG.md index 86830f3..e6e2a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * 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 @@ -12,6 +13,7 @@ * 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 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 b600748..ab40ae7 100644 --- a/ProvisionQL.xcodeproj/project.pbxproj +++ b/ProvisionQL.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ 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 */; }; @@ -21,8 +24,6 @@ 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 */; }; @@ -38,6 +39,9 @@ /* 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 = ""; }; @@ -52,8 +56,6 @@ 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 = ""; }; @@ -84,6 +86,7 @@ 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 */, @@ -93,6 +96,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 54768CE82B69BB27007E81D2 /* PrivateFrameworks */ = { + isa = PBXGroup; + children = ( + 54768D042B701D6C007E81D2 /* CoreUI.framework */, + ); + path = PrivateFrameworks; + sourceTree = ""; + }; 54F4EAFB2B6668940000CE41 /* 3rd-party */ = { isa = PBXGroup; children = ( @@ -112,15 +123,6 @@ path = pinch; sourceTree = ""; }; - 55424C641870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect */ = { - isa = PBXGroup; - children = ( - 55424C651870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.h */, - 55424C661870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.m */, - ); - path = "NSBezierPath+IOS7RoundedRect"; - sourceTree = ""; - }; 55457C10203C4A7500ED02E5 /* Metadata */ = { isa = PBXGroup; children = ( @@ -161,8 +163,8 @@ 55DB7272186E193500CAFEE7 = { isa = PBXGroup; children = ( + 54768CE82B69BB27007E81D2 /* PrivateFrameworks */, 55457C10203C4A7500ED02E5 /* Metadata */, - 55424C641870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect */, 55DB7288186E193500CAFEE7 /* ProvisionQL */, 55DB727F186E193500CAFEE7 /* Frameworks */, 55DB727E186E193500CAFEE7 /* Products */, @@ -201,6 +203,8 @@ 555E9518186E2DC0001D406A /* Resources */, 54F4EB662B6719310000CE41 /* AppCategories.h */, 54F4EB672B6719310000CE41 /* AppCategories.m */, + 54768CF82B6D3841007E81D2 /* AppIcon.h */, + 54768CF92B6D3841007E81D2 /* AppIcon.m */, 54B1E0092B6989E7009E654A /* Entitlements.h */, 54B1E00A2B6989E7009E654A /* Entitlements.m */, 557C842718733828008A2A0C /* Shared.h */, @@ -221,7 +225,7 @@ buildActionMask = 2147483647; files = ( 54F4EB682B6719310000CE41 /* AppCategories.h in Headers */, - 55424C671870DB2A002F5408 /* NSBezierPath+IOS7RoundedRect.h in Headers */, + 54768CFA2B6D3841007E81D2 /* AppIcon.h in Headers */, 54B1E00B2B6989E7009E654A /* Entitlements.h in Headers */, 54F4EB002B6668A50000CE41 /* ZipEntry.h in Headers */, 557C842818733828008A2A0C /* Shared.h in Headers */, @@ -332,7 +336,7 @@ 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 */, @@ -455,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.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; @@ -475,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.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/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/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index 99314f8..c636931 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -1,5 +1,6 @@ #import "Shared.h" #import "AppCategories.h" +#import "AppIcon.h" #import "Entitlements.h" // makro to stop further processing @@ -72,15 +73,6 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla return stringToEscape; } -/// Convert image to PNG and encode with base64 to be embeded in html output. -NSString * _Nonnull iconAsBase64(NSImage *appIcon) { - appIcon = roundCorners(appIcon); - NSData *imageData = [appIcon TIFFRepresentation]; - NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData]; - imageData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; - return [imageData base64EncodedStringWithOptions:0]; -} - // MARK: - Date processing @@ -618,10 +610,6 @@ OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, [infoLayer addEntriesFromDictionary:procProvision(plistProvision, meta.isOSX)]; ALLOW_EXIT - // App Icon - infoLayer[@"AppIcon"] = iconAsBase64(imageFromApp(meta, plistApp)); - ALLOW_EXIT - // Entitlements [infoLayer addEntriesFromDictionary:procEntitlements(meta, plistApp, plistProvision)]; ALLOW_EXIT @@ -633,6 +621,13 @@ OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, [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); diff --git a/ProvisionQL/GenerateThumbnailForURL.m b/ProvisionQL/GenerateThumbnailForURL.m index b20ad99..a94be2e 100644 --- a/ProvisionQL/GenerateThumbnailForURL.m +++ b/ProvisionQL/GenerateThumbnailForURL.m @@ -1,4 +1,5 @@ #import "Shared.h" +#import "AppIcon.h" // makro to stop further processing #define ALLOW_EXIT if (QLThumbnailRequestIsCancelled(thumbnail)) { return noErr; } @@ -30,17 +31,10 @@ // MARK: .ipa .xcarchive OSStatus renderAppIcon(QuickLookInfo meta, QLThumbnailRequestRef thumbnail) { - NSImage *appIcon = imageFromApp(meta, nil); - ALLOW_EXIT - - // if downscale, then this should respect retina resolution -// CGSize maxSize = QLThumbnailRequestGetMaximumSize(thumbnail); -// if (appIcon.size.width > maxSize.width && appIcon.size.height > maxSize.height) { -// [appIcon setSize:maxSize]; -// } - - appIcon = roundCorners(appIcon); - ALLOW_EXIT + AppIcon *icon = [AppIcon load:meta]; + if (!icon.canExtractImage) { + return noErr; + } // set magic flag to draw icon without additional markers static const NSString *IconFlavor; @@ -58,6 +52,9 @@ OSStatus renderAppIcon(QuickLookInfo meta, QLThumbnailRequestRef thumbnail) { 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; @@ -147,12 +144,12 @@ OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thum @autoreleasepool { QuickLookInfo meta = initQLInfo(contentTypeUTI, url); - if (meta.type == FileTypeIPA || meta.type == FileTypeArchive) { - return renderAppIcon(meta, thumbnail); - } else if (meta.type == FileTypeProvision) { + 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; diff --git a/ProvisionQL/Shared.h b/ProvisionQL/Shared.h index 6e572cf..a372b5c 100644 --- a/ProvisionQL/Shared.h +++ b/ProvisionQL/Shared.h @@ -6,8 +6,6 @@ #import #import -#import - #import "ZipFile.h" static NSString * _Nonnull const kPluginBundleId = @"com.ealeksandrov.ProvisionQL"; @@ -39,6 +37,7 @@ typedef struct QuickLookMeta { 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); @@ -52,7 +51,3 @@ typedef NS_ENUM(NSUInteger, ExpirationStatus) { ExpirationStatus expirationStatus(NSDate * _Nullable date); NSDate * _Nullable dateOrNil(NSDate * _Nullable value); NSArray * _Nullable arrayOrNil(NSArray * _Nullable value); - -// App Icon -NSImage * _Nonnull roundCorners(NSImage * _Nonnull image); -NSImage * _Nonnull imageFromApp(QuickLookInfo meta, NSDictionary * _Nullable appPlist); diff --git a/ProvisionQL/Shared.m b/ProvisionQL/Shared.m index 5f6c63a..23bb0e0 100644 --- a/ProvisionQL/Shared.m +++ b/ProvisionQL/Shared.m @@ -131,103 +131,3 @@ ExpirationStatus expirationStatus(NSDate *date) { inline NSArray * _Nullable arrayOrNil(NSArray * _Nullable value) { return [value isKindOfClass:[NSArray class]] ? value : nil; } - - -// MARK: - App Icon - -/// Apply rounded corners to image (iOS7 style) -NSImage * _Nonnull roundCorners(NSImage *image) { - NSImage *existingImage = image; - NSSize existingSize = [existingImage 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]; - - [image drawAtPoint:NSZeroPoint fromRect:NSMakeRect(0, 0, existingSize.width, existingSize.height) operation:NSCompositingOperationSourceOver fraction:1]; - - [composedImage unlockFocus]; - - return composedImage; -} - -/// Given a list of filenames, try to find the one with the highest resolution -NSString *selectBestIcon(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 - return [icons lastObject]; -} - -/// Deep select icons from plist key @c CFBundleIcons and @c CFBundleIcons~ipad -NSArray * _Nullable iconsListForDictionary(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; - } - } - } - return nil; -} - -/// Parse app plist to find the bundle icon filename. -NSString * _Nullable mainIconNameForApp(NSDictionary *appPlist) { - //Check for CFBundleIcons (since 5.0) - NSArray *icons = iconsListForDictionary(appPlist[@"CFBundleIcons"]); - if (!icons) { - icons = iconsListForDictionary(appPlist[@"CFBundleIcons~ipad"]); - if (!icons) { - //Check for CFBundleIconFiles (since 3.2) - icons = arrayOrNil(appPlist[@"CFBundleIconFiles"]); - if (!icons) { - //Check for CFBundleIconFile (legacy, before 3.2) - return appPlist[@"CFBundleIconFile"]; // may be nil - } - } - } - return selectBestIcon(icons); -} - -/// Depending on the file type, find the icon within the bundle -/// @param appPlist If @c nil, will load plist on the fly (used for thumbnail) -NSImage * _Nonnull imageFromApp(QuickLookInfo meta, NSDictionary *appPlist) { - if (meta.type == FileTypeIPA) { - NSData *data = [meta.zipFile unzipFile:@"iTunesArtwork"]; - if (!data) { - NSString *fileName = mainIconNameForApp(appPlist ?: readPlistApp(meta)); - if (fileName) { - data = [meta.zipFile unzipFile:[NSString stringWithFormat:@"Payload/*.app/%@*", fileName]]; - } - // TODO: load assets.car - } - if (data) { - return [[NSImage alloc] initWithData:data]; - } - } else if (meta.type == FileTypeArchive) { - // get the embedded icon for the iOS app - NSString *fileName = mainIconNameForApp(appPlist ?: readPlistApp(meta)); - NSArray *appContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:meta.effectiveUrl.path error:nil]; - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF contains %@", fileName]; - NSString *matchedName = [appContents filteredArrayUsingPredicate:predicate].lastObject; - if (matchedName) { - NSURL *appIconFullURL = [meta.effectiveUrl URLByAppendingPathComponent:matchedName]; - return [[NSImage alloc] initWithContentsOfURL:appIconFullURL]; - } - } - // Fallback to default icon - NSURL *iconURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"defaultIcon" withExtension:@"png"]; - return [[NSImage alloc] initWithContentsOfURL:iconURL]; -} diff --git a/ProvisionQL/ZipFile.h b/ProvisionQL/ZipFile.h index f87d33b..c178423 100644 --- a/ProvisionQL/ZipFile.h +++ b/ProvisionQL/ZipFile.h @@ -1,4 +1,5 @@ #import +@class ZipEntry; NS_ASSUME_NONNULL_BEGIN @@ -6,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN + (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 index 9fe9d97..4ad3727 100644 --- a/ProvisionQL/ZipFile.m +++ b/ProvisionQL/ZipFile.m @@ -25,6 +25,14 @@ - (instancetype)initWithFile:(NSString *)path { // 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 { From f60808c5e2caeca48bca6dd782e7a262f9c88e8a Mon Sep 17 00:00:00 2001 From: relikd Date: Sun, 4 Feb 2024 21:53:03 +0100 Subject: [PATCH 20/35] chore: script to test ProvisionQL on multiple files --- ProvisionQL/Scripts/test_generate_all.sh | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100755 ProvisionQL/Scripts/test_generate_all.sh 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' From 28c9fd5997500f8b3f971800d5029765e4105e83 Mon Sep 17 00:00:00 2001 From: relikd Date: Wed, 7 Feb 2024 15:37:25 +0100 Subject: [PATCH 21/35] fix: crash if binary cant be unzipped (utf8 chars) --- ProvisionQL/Entitlements.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ProvisionQL/Entitlements.m b/ProvisionQL/Entitlements.m index 89f085f..7c9d0bc 100644 --- a/ProvisionQL/Entitlements.m +++ b/ProvisionQL/Entitlements.m @@ -28,6 +28,10 @@ + (instancetype)withBinary:(NSString * _Nonnull)appBinaryPath { - (instancetype)initWithBinaryPath:(NSString * _Nonnull)path { self = [super init]; if (self) { + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + NSLog(@"WARN: provided binary '%@' does not exist (unzip error?).", [path lastPathComponent]); + return self; + } _binaryPath = path; _plist = [self getSecCodeEntitlements]; if (!_plist) { From 9e48576d5c9b2adaf2517ea01aae2d7639aee4e3 Mon Sep 17 00:00:00 2001 From: relikd Date: Wed, 7 Feb 2024 16:20:16 +0100 Subject: [PATCH 22/35] fix: reintroduce unzip exclude matching --- ProvisionQL/ZipFile.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ProvisionQL/ZipFile.m b/ProvisionQL/ZipFile.m index 4ad3727..4792088 100644 --- a/ProvisionQL/ZipFile.m +++ b/ProvisionQL/ZipFile.m @@ -84,7 +84,7 @@ - (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 setArguments:@[@"-p", self.pathToZipFile, filePath, @"-x", @"*/*/*/*"]]; [task launch]; #ifdef DEBUG @@ -102,7 +102,7 @@ - (NSData * _Nullable)sysUnzipFile:(NSString *)filePath { - (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 setArguments:@[@"-u", @"-j", @"-d", targetDir, self.pathToZipFile, filePath, @"-x", @"*/*/*/*"]]; [task launch]; #ifdef DEBUG From 16e7c1c7b2c22bc5f8e0fcf398ffaf70e7fcbf16 Mon Sep 17 00:00:00 2001 From: relikd Date: Sun, 11 Feb 2024 13:58:09 +0100 Subject: [PATCH 23/35] fix: fallback to device type iPhone --- ProvisionQL/GeneratePreviewForURL.m | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ProvisionQL/GeneratePreviewForURL.m b/ProvisionQL/GeneratePreviewForURL.m index c636931..9e75dc2 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -231,6 +231,13 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla } } + NSString *minVersion = appPlist[@"MinimumOSVersion"]; + if (platforms.count == 0) { + if ([minVersion hasPrefix:@"1."] || [minVersion hasPrefix:@"2."] || [minVersion hasPrefix:@"3."]) { + [platforms addObject:@"iPhone"]; + } + } + return @{ @"AppInfoHidden": @"", @"ProvisionTitleHidden": @"hiddenDiv", @@ -245,7 +252,7 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla @"UIDeviceFamily": [platforms componentsJoinedByString:@", "], @"DTSDKName": appPlist[@"DTSDKName"] ?: @"", - @"MinimumOSVersion": appPlist[@"MinimumOSVersion"] ?: @"", + @"MinimumOSVersion": minVersion ?: @"", @"AppTransportSecurityFormatted": formattedAppTransportSecurity(appPlist), }; } From 59cbc68702e9c6ea681b41e102c22cb701df21f3 Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 12 Feb 2024 01:04:34 +0100 Subject: [PATCH 24/35] style: indent with spaces --- ProvisionQL/3rd-party/pinch/ZipEntry.h | 12 +- ProvisionQL/3rd-party/pinch/ZipEntry.m | 4 +- ProvisionQL/3rd-party/pinch/pinch.m | 424 +++++------ ProvisionQL/AppCategories.m | 306 ++++---- ProvisionQL/AppIcon.m | 476 ++++++------ ProvisionQL/Entitlements.m | 296 ++++---- ProvisionQL/GeneratePreviewForURL.m | 974 ++++++++++++------------- ProvisionQL/GenerateThumbnailForURL.m | 222 +++--- ProvisionQL/Resources/template.html | 288 ++++---- ProvisionQL/Shared.h | 28 +- ProvisionQL/Shared.m | 170 ++--- ProvisionQL/ZipFile.m | 122 ++-- 12 files changed, 1661 insertions(+), 1661 deletions(-) diff --git a/ProvisionQL/3rd-party/pinch/ZipEntry.h b/ProvisionQL/3rd-party/pinch/ZipEntry.h index 777c357..77ae0e2 100755 --- a/ProvisionQL/3rd-party/pinch/ZipEntry.h +++ b/ProvisionQL/3rd-party/pinch/ZipEntry.h @@ -1,23 +1,23 @@ /*--------------------------------------------------------------------------- - + 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 @@ -25,7 +25,7 @@ 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 diff --git a/ProvisionQL/3rd-party/pinch/ZipEntry.m b/ProvisionQL/3rd-party/pinch/ZipEntry.m index ca54491..769f487 100755 --- a/ProvisionQL/3rd-party/pinch/ZipEntry.m +++ b/ProvisionQL/3rd-party/pinch/ZipEntry.m @@ -49,8 +49,8 @@ @implementation ZipEntry @implementation NSArray (ZipEntry) - (ZipEntry*)zipEntryWithPath:(NSString*)path { - NSPredicate *pred = [NSPredicate predicateWithFormat:@"filepath LIKE %@", path]; - return [self filteredArrayUsingPredicate:pred].firstObject; + NSPredicate *pred = [NSPredicate predicateWithFormat:@"filepath LIKE %@", path]; + return [self filteredArrayUsingPredicate:pred].firstObject; } @end diff --git a/ProvisionQL/3rd-party/pinch/pinch.m b/ProvisionQL/3rd-party/pinch/pinch.m index f333b44..a568f3b 100755 --- a/ProvisionQL/3rd-party/pinch/pinch.m +++ b/ProvisionQL/3rd-party/pinch/pinch.m @@ -43,162 +43,162 @@ of this software and associated documentation files (the "Software"), to deal // 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; + 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; + 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; + 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; + 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 + 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 ); + 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; + 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; + 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; + 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]; + zstream.avail_in = entry.sizeCompressed; + zstream.next_in = &cptr[idx + file_record.fileNameLength + file_record.extraFieldLength]; - unsigned char *ptr = malloc(entry.sizeUncompressed); + unsigned char *ptr = malloc(entry.sizeUncompressed); - zstream.avail_out = entry.sizeUncompressed; - zstream.next_out = ptr; + zstream.avail_out = entry.sizeUncompressed; + zstream.next_out = ptr; - ret = inflate(&zstream, Z_SYNC_FLUSH); + ret = inflate(&zstream, Z_SYNC_FLUSH); - if (isValid(ptr, entry.sizeUncompressed, file_record.CRC32)) { - outputData = [NSData dataWithBytes:ptr length:entry.sizeUncompressed]; - } + if (isValid(ptr, entry.sizeUncompressed, file_record.CRC32)) { + outputData = [NSData dataWithBytes:ptr length:entry.sizeUncompressed]; + } - free(ptr); + 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; - ; - } + // 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); + inflateEnd(&zstream); - } else if (entry.method == 0) { + } else if (entry.method == 0) { - unsigned char *ptr = &cptr[idx + file_record.fileNameLength + file_record.extraFieldLength]; + 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]; - } + if (isValid(ptr, entry.sizeUncompressed, file_record.CRC32)) { + outputData = [NSData dataWithBytes:ptr length:entry.sizeUncompressed]; + } - } else { - NSLog(@"WARN: unimplemented compression method: %d", entry.method); - } + } else { + NSLog(@"WARN: unimplemented compression method: %d", entry.method); + } - return outputData; + return outputData; } @@ -206,124 +206,124 @@ BOOL isValid(unsigned char *ptr, int lenUncompressed, uint32 expectedCrc32) { /// 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]; + unsigned long long eof = [fp seekToEndOfFile]; + [fp seekToFileOffset:MAX(0, eof - 4096)]; + NSData *data = [fp readDataToEndOfFile]; - char centralDirSignature[4] = { - 0x50, 0x4b, 0x05, 0x06 - }; + char centralDirSignature[4] = { + 0x50, 0x4b, 0x05, 0x06 + }; - const char *cptr = (const char*)[data bytes]; - long len = [data length]; - char *found = NULL; + const char *cptr = (const char*)[data bytes]; + long len = [data length]; + char *found = NULL; - do { - char *fptr = memchr(cptr, 0x50, len); + do { + char *fptr = memchr(cptr, 0x50, len); - if (!fptr) // done searching - break; + if (!fptr) // done searching + break; - // Use the last found directory - if (!memcmp(centralDirSignature, fptr, 4)) - found = fptr; + // Use the last found directory + if (!memcmp(centralDirSignature, fptr, 4)) + found = fptr; - len = len - (fptr - cptr) - 1; - cptr = fptr + 1; - } while (1); + 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; - } + 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 + 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 ); + GETFIELD( endOfCentralDirectorySignature ); + GETFIELD( numberOfThisDisk ); + GETFIELD( diskWhereCentralDirectoryStarts ); + GETFIELD( numberOfCentralDirectoryRecordsOnThisDisk ); + GETFIELD( totalNumberOfCentralDirectoryRecords ); + GETFIELD( sizeOfCentralDirectory ); + GETFIELD( offsetOfStartOfCentralDirectory ); + GETFIELD( ZIPfileCommentLength ); #undef GETFIELD - return end_record; + 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]; + [fp seekToFileOffset:end_record.offsetOfStartOfCentralDirectory]; + NSData *data = [fp readDataOfLength:end_record.sizeOfCentralDirectory]; - const char *cptr = (const char*)[data bytes]; - long len = [data length]; + 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 + // 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; + 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 + // 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 ); + 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; + 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; + 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.m b/ProvisionQL/AppCategories.m index 80508da..7a42090 100644 --- a/ProvisionQL/AppCategories.m +++ b/ProvisionQL/AppCategories.m @@ -1,163 +1,163 @@ #import "AppCategories.h" /* - #!/usr/bin/env python3 - # download: https://itunes.apple.com/WebObjects/MZStoreServices.woa/ws/genres - import json - ids = {} +#!/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']) +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']) +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)) - */ +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", + 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; + // 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.m b/ProvisionQL/AppIcon.m index 3825792..f792989 100644 --- a/ProvisionQL/AppIcon.m +++ b/ProvisionQL/AppIcon.m @@ -18,15 +18,15 @@ @interface AppIcon() @implementation AppIcon + (instancetype)load:(QuickLookInfo)meta { - return [[self alloc] initWithMeta:meta]; + return [[self alloc] initWithMeta:meta]; } - (instancetype)initWithMeta:(QuickLookInfo)meta { - self = [super init]; - if (self) { - _meta = meta; - } - return self; + self = [super init]; + if (self) { + _meta = meta; + } + return self; } @@ -34,15 +34,15 @@ - (instancetype)initWithMeta:(QuickLookInfo)meta { /// 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; + switch (_meta.type) { + case FileTypeIPA: + case FileTypeArchive: + case FileTypeExtension: + return YES; + case FileTypeProvision: + return NO; + } + return NO; } @@ -51,80 +51,80 @@ - (BOOL)canExtractImage { /// 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) { + // 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."); + NSLog(@"[icon] using iTunesArtwork."); #endif - return [[NSImage alloc] initWithData:data]; - } - } + return [[NSImage alloc] initWithData:data]; + } + } - // Extract image name from app plist - NSString *plistImgName = [self iconNameFromPlist:appPlist]; + // Extract image name from app plist + NSString *plistImgName = [self iconNameFromPlist:appPlist]; #ifdef DEBUG - NSLog(@"[icon] icon name: %@", plistImgName); + 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) { + 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); + 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 + 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); - } + @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]; + // 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) { + 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); + NSLog(@"[icon] using Assets.car with key %@", validName); #endif - return [[NSImage alloc] initWithCGImage:bestImage.image size:bestImage.size]; - } - } - return nil; + return [[NSImage alloc] initWithCGImage:bestImage.image size:bestImage.size]; + } + } + return nil; } @@ -132,63 +132,63 @@ - (NSImage * _Nullable)imageFromAssetsCar:(NSString *)imageName { /// 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; + 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; + 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; + 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 @@ -200,90 +200,90 @@ - (CUINamedImage * _Nullable)carFindHighestResolutionIcon:(NSArray 0) { - return [self findHighestResolutionIconName:matchingNames]; - } - return nil; + 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; + 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; + 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 @@ -308,33 +308,33 @@ @implementation NSBezierPath (IOS7RoundedRect) /// 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; + 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 @@ -347,37 +347,37 @@ @implementation NSImage (AppIcon) /// Apply rounded corners to image (iOS7 style) - (NSImage * _Nonnull)withRoundCorners { - NSSize existingSize = [self size]; - NSImage *composedImage = [[NSImage alloc] initWithSize:existingSize]; + NSSize existingSize = [self size]; + NSImage *composedImage = [[NSImage alloc] initWithSize:existingSize]; - [composedImage lockFocus]; - [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh]; + [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]; + 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; + [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]; + // 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]; - } + // 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.m b/ProvisionQL/Entitlements.m index 7c9d0bc..b76e0a2 100644 --- a/ProvisionQL/Entitlements.m +++ b/ProvisionQL/Entitlements.m @@ -16,59 +16,59 @@ @implementation Entitlements /// Use provision plist data without running @c codesign or + (instancetype)withoutBinary { - return [[self alloc] init]; + 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]; + return [[self alloc] initWithBinaryPath:appBinaryPath]; } - (instancetype)initWithBinaryPath:(NSString * _Nonnull)path { - self = [super init]; - if (self) { - if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { - NSLog(@"WARN: provided binary '%@' does not exist (unzip error?).", [path lastPathComponent]); - return self; - } - _binaryPath = path; - _plist = [self getSecCodeEntitlements]; - if (!_plist) { - _plist = [self sysCallCodeSign]; // fallback to system call - } - } - return self; + self = [super init]; + if (self) { + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + NSLog(@"WARN: provided binary '%@' does not exist (unzip error?).", [path lastPathComponent]); + return 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]]) { + // 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"); + NSLog(@"[entitlements] fallback to provision plist entitlements"); #endif - _plist = fallbackEntitlementsPlist; - } - } - _html = [self format:_plist]; - _plist = nil; // free memory - _codeSignError = nil; + _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 + if (plist) { + NSMutableString *output = [NSMutableString string]; + recursiveKeyValue(0, nil, plist, output); + return [NSString stringWithFormat:@"
%@
", output]; + } + return _codeSignError; // may be nil } @@ -76,52 +76,52 @@ - (NSString * _Nullable)format:(NSDictionary *)plist { /// 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) { + 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"); + 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) { + 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"); + 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; + 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; } @@ -129,43 +129,43 @@ - (NSDictionary *)getSecCodeEntitlements { /// 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]; - + 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:@" "]); + 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; + + 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 @@ -175,42 +175,42 @@ - (NSDictionary *)sysCallCodeSign { /// 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]; - } - } + 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 9e75dc2..feb7f04 100644 --- a/ProvisionQL/GeneratePreviewForURL.m +++ b/ProvisionQL/GeneratePreviewForURL.m @@ -22,55 +22,55 @@ /// 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; + 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) ? @"- " : @"  "]; - } - - 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]; - } - } + 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) ? @"- " : @"  "]; + } + + 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]; + } + } } /// 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; + 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; } @@ -78,96 +78,96 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla /// @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 [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]; + 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]; + 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; + 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)]; + 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 [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)]]; + 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"; - } + switch (expirationStatus(date)) { + case ExpirationStatusExpired: return @"expired"; + case ExpirationStatusExpiring: return @"expiring"; + case ExpirationStatusValid: return @"valid"; + } } @@ -175,86 +175,86 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla /// @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"; + 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; - } - } - - NSString *minVersion = appPlist[@"MinimumOSVersion"]; - if (platforms.count == 0) { - if ([minVersion hasPrefix:@"1."] || [minVersion hasPrefix:@"2."] || [minVersion hasPrefix:@"3."]) { - [platforms addObject:@"iPhone"]; - } - } - - 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": minVersion ?: @"", - @"AppTransportSecurityFormatted": formattedAppTransportSecurity(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; + } + } + + NSString *minVersion = appPlist[@"MinimumOSVersion"]; + if (platforms.count == 0) { + if ([minVersion hasPrefix:@"1."] || [minVersion hasPrefix:@"2."] || [minVersion hasPrefix:@"3."]) { + [platforms addObject:@"iPhone"]; + } + } + + 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": minVersion ?: @"", + @"AppTransportSecurityFormatted": formattedAppTransportSecurity(appPlist), + }; } @@ -262,56 +262,56 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla /// 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:@", "]; + 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"] ?: @"", - }; + 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"] ?: @"", + }; } @@ -320,60 +320,60 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla /// 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 { - NSLog(@"No invalidity values in '%@' certificate, dictionary = %@", subject, outerDictRef); - } - CFRelease(outerDictRef); - } else { - NSLog(@"Could not get values in '%@' certificate, error = %@", subject, error); - CFRelease(error); - } - return invalidityDate; + 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 { + NSLog(@"No invalidity values in '%@' certificate, dictionary = %@", subject, outerDictRef); + } + CFRelease(outerDictRef); + } else { + 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 { - NSLog(@"Could not get subject from certificate"); - } - CFRelease(certificateRef); - } - - [entries sortUsingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) { - return [obj1[0] compare:obj2[0]]; - }]; - return entries; + 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 { + NSLog(@"Could not get subject from certificate"); + } + CFRelease(certificateRef); + } + + [entries sortUsingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) { + return [obj1[0] compare:obj2[0]]; + }]; + return entries; } @@ -381,71 +381,71 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla /// 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)"; + 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)"; } /// 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; + 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", - }; + 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", + }; } @@ -453,45 +453,45 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla /// 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; - } - - Entitlements *rv = [Entitlements withBinary:[basePath stringByAppendingPathComponent:bundleExecutable]]; - if (currentTempDirFolder) { - [[NSFileManager defaultManager] removeItemAtPath:currentTempDirFolder error:nil]; - } - return rv; + 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; + } + + Entitlements *rv = [Entitlements withBinary:[basePath stringByAppendingPathComponent:bundleExecutable]]; + if (currentTempDirFolder) { + [[NSFileManager defaultManager] removeItemAtPath:currentTempDirFolder error:nil]; + } + return rv; } /// 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"]]; + Entitlements *entitlements = readEntitlements(meta, appPlist[@"CFBundleExecutable"]); + [entitlements applyFallbackIfNeeded:provisionPlist[@"Entitlements"]]; - return @{ - @"EntitlementsWarningHidden": entitlements.hasError ? @"" : @"hiddenDiv", - @"EntitlementsFormatted": entitlements.html ?: @"No Entitlements", - }; + return @{ + @"EntitlementsWarningHidden": entitlements.hasError ? @"" : @"hiddenDiv", + @"EntitlementsFormatted": entitlements.html ?: @"No Entitlements", + }; } @@ -499,46 +499,46 @@ void recursiveDictWithReplacements(NSDictionary *dictionary, NSDictionary *repla /// 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; + 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; } /// 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; + 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; } /// 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 ?: @"", - }; + 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 ?: @"", + }; } @@ -546,108 +546,108 @@ unsigned long long getFileSize(NSString *path) { /// Process meta information about the plugin. Like version and debug flag. NSDictionary * _Nonnull procFooterInfo() { - NSBundle *mainBundle = [NSBundle bundleWithIdentifier:kPluginBundleId]; - return @{ + NSBundle *mainBundle = [NSBundle bundleWithIdentifier:kPluginBundleId]; + return @{ #ifdef DEBUG - @"DEBUG": @"(debug)", + @"DEBUG": @"(debug)", #else - @"DEBUG": @"", + @"DEBUG": @"", #endif - @"BundleShortVersionString": [mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"", - @"BundleVersion": [mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"] ?: @"", - }; + @"BundleShortVersionString": [mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"", + @"BundleVersion": [mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"] ?: @"", + }; } // 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; + 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; } 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); - - // App Info - NSDictionary *plistApp = readPlistApp(meta); - [infoLayer addEntriesFromDictionary:procAppInfo(plistApp)]; - ALLOW_EXIT - - 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; + @autoreleasepool { + QuickLookInfo meta = initQLInfo(contentTypeUTI, url); + if (!meta.type) { + return noErr; + } + NSMutableDictionary* infoLayer = [NSMutableDictionary dictionary]; + infoLayer[@"AppInfoTitle"] = stringForFileType(meta); + + // App Info + NSDictionary *plistApp = readPlistApp(meta); + [infoLayer addEntriesFromDictionary:procAppInfo(plistApp)]; + ALLOW_EXIT + + 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 CancelPreviewGeneration(void *thisInterface, QLPreviewRequestRef preview) { - // Implement only if supported + // Implement only if supported } diff --git a/ProvisionQL/GenerateThumbnailForURL.m b/ProvisionQL/GenerateThumbnailForURL.m index a94be2e..a6f57f3 100644 --- a/ProvisionQL/GenerateThumbnailForURL.m +++ b/ProvisionQL/GenerateThumbnailForURL.m @@ -31,130 +31,130 @@ // 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; + 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; + NSDictionary *propertyList = readPlistProvision(meta); + ALLOW_EXIT + + NSUInteger devicesCount = arrayOrNil(propertyList[@"ProvisionedDevices"]).count; + NSDate *expirationDate = dateOrNil(propertyList[@"ExpirationDate"]); + + NSImage *appIcon = nil; + if (iconMode) { + NSURL *iconURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"blankIcon" withExtension:@"png"]; + appIcon = [[NSImage alloc] initWithContentsOfURL:iconURL]; + } else { + appIcon = [[NSWorkspace sharedWorkspace] iconForFileType:meta.UTI]; + [appIcon setSize:NSMakeSize(512, 512)]; + } + ALLOW_EXIT + + NSRect renderRect = NSMakeRect(0.0, 0.0, appIcon.size.width, appIcon.size.height); + + // Font attributes + NSColor *outlineColor; + switch (expirationStatus(expirationDate)) { + case ExpirationStatusExpired: outlineColor = BADGE_EXPIRED_COLOR; break; + case ExpirationStatusExpiring: outlineColor = BADGE_EXPIRING_COLOR; break; + case ExpirationStatusValid: outlineColor = BADGE_VALID_COLOR; break; + } + + NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail; + paragraphStyle.alignment = NSTextAlignmentCenter; + + NSDictionary *fontAttrs = @{ + NSFontAttributeName : BADGE_FONT, + NSForegroundColorAttributeName : outlineColor, + NSParagraphStyleAttributeName: paragraphStyle + }; + + // Badge size & placement + int badgeX = renderRect.origin.x + BADGE_MARGIN_X; + int badgeY = renderRect.origin.y + renderRect.size.height - BADGE_HEIGHT - BADGE_MARGIN_Y; + if (!iconMode) { + badgeX += 75; + badgeY -= 10; + } + int badgeNumX = badgeX + BADGE_MARGIN; + NSPoint badgeTextPoint = NSMakePoint(badgeNumX, badgeY); + + NSString *badge = [NSString stringWithFormat:@"%lu",(unsigned long)devicesCount]; + NSSize badgeNumSize = [badge sizeWithAttributes:fontAttrs]; + int badgeWidth = badgeNumSize.width + BADGE_MARGIN * 2; + NSRect badgeOutlineRect = NSMakeRect(badgeX, badgeY, MAX(badgeWidth, MIN_BADGE_WIDTH), BADGE_HEIGHT); + + // Do as much work as possible before the `CreateContext`. We can try to quit early before that! + CGContextRef _context = QLThumbnailRequestCreateContext(thumbnail, renderRect.size, false, NULL); + if (_context) { + NSGraphicsContext *_graphicsContext = [NSGraphicsContext graphicsContextWithCGContext:(void *)_context flipped:NO]; + [NSGraphicsContext setCurrentContext:_graphicsContext]; + [appIcon drawInRect:renderRect]; + + NSBezierPath *badgePath = [NSBezierPath bezierPathWithRoundedRect:badgeOutlineRect xRadius:10 yRadius:10]; + [badgePath setLineWidth:8.0]; + [BADGE_BG_COLOR set]; + [badgePath fill]; + [outlineColor set]; + [badgePath stroke]; + + [badge drawAtPoint:badgeTextPoint withAttributes:fontAttrs]; + + QLThumbnailRequestFlushContext(thumbnail, _context); + CFRelease(_context); + } + return noErr; } // MARK: Main Entry OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thumbnail, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options, CGSize maxSize) { - @autoreleasepool { - QuickLookInfo meta = initQLInfo(contentTypeUTI, url); - - if (meta.type == FileTypeProvision) { - NSDictionary *optionsDict = (__bridge NSDictionary *)options; - BOOL iconMode = ([optionsDict objectForKey:(NSString *)kQLThumbnailOptionIconModeKey]) ? YES : NO; - return renderProvision(meta, thumbnail, iconMode); - } else { - return renderAppIcon(meta, thumbnail); - } - } - return noErr; + @autoreleasepool { + QuickLookInfo meta = initQLInfo(contentTypeUTI, url); + + if (meta.type == FileTypeProvision) { + NSDictionary *optionsDict = (__bridge NSDictionary *)options; + BOOL iconMode = ([optionsDict objectForKey:(NSString *)kQLThumbnailOptionIconModeKey]) ? YES : NO; + return renderProvision(meta, thumbnail, iconMode); + } else { + return renderAppIcon(meta, thumbnail); + } + } + return noErr; } void CancelThumbnailGeneration(void *thisInterface, QLThumbnailRequestRef thumbnail) { - // Implement only if supported + // Implement only if supported } diff --git a/ProvisionQL/Resources/template.html b/ProvisionQL/Resources/template.html index 8e02dda..44cfd42 100644 --- a/ProvisionQL/Resources/template.html +++ b/ProvisionQL/Resources/template.html @@ -1,51 +1,51 @@ - - - - - - -
-

__AppInfoTitle__

-
App icon
-
- Name: __CFBundleName__
- Version: __CFBundleShortVersionString__ (__CFBundleVersion__)
- BundleId: __CFBundleIdentifier__
-
- Extension type: __ExtensionType__
-
- DeviceFamily: __UIDeviceFamily__
- 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__
- Expiration Date: __ExpirationDateFormatted__
-
- -
-

Entitlements

-
- Entitlements extraction failed. -
- __EntitlementsFormatted__ -
- -
-

Developer Certificates

- __DeveloperCertificatesFormatted__ -
- -
-

Devices (__ProvisionedDevicesCount__)

- __ProvisionedDevicesFormatted__ -
- -
-

iTunes Metadata

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

File info

- __FileName__
- __FileInfo__
-
- - + body { + background: #323232; + color: #fff; + } + + a { color: #aaa; } + a:hover { color: #fff; } + a:visited { color: #aaa; } + + .expired, .warning { + color: red; + } + .expiring { + color: orange; + } + .valid { + color: lightgreen; + } + + tr:nth-child(odd) { + background-color: #1e1e1e; + } + + tr:nth-child(even) { + background-color: #292929; + } +} + + + +
+

__AppInfoTitle__

+
App icon
+
+ Name: __CFBundleName__
+ Version: __CFBundleShortVersionString__ (__CFBundleVersion__)
+ BundleId: __CFBundleIdentifier__
+
+ Extension type: __ExtensionType__
+
+ DeviceFamily: __UIDeviceFamily__
+ 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__
+ Expiration Date: __ExpirationDateFormatted__
+
+ +
+

Entitlements

+
+ Entitlements extraction failed. +
+ __EntitlementsFormatted__ +
+ +
+

Developer Certificates

+ __DeveloperCertificatesFormatted__ +
+ +
+

Devices (__ProvisionedDevicesCount__)

+ __ProvisionedDevicesFormatted__ +
+ +
+

iTunes Metadata

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

File info

+ __FileName__
+ __FileInfo__
+
+ + diff --git a/ProvisionQL/Shared.h b/ProvisionQL/Shared.h index a372b5c..46a6d26 100644 --- a/ProvisionQL/Shared.h +++ b/ProvisionQL/Shared.h @@ -18,20 +18,20 @@ static NSString * _Nonnull const kDataType_app_extension = @"com.apple.appli // Init QuickLook Type typedef NS_ENUM(NSUInteger, FileType) { - FileTypeIPA = 1, - FileTypeArchive, - FileTypeExtension, - FileTypeProvision, + 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 + 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); @@ -44,9 +44,9 @@ NSDictionary * _Nullable readPlistItunes(QuickLookInfo meta); // Other helper typedef NS_ENUM(NSUInteger, ExpirationStatus) { - ExpirationStatusExpired = 0, - ExpirationStatusExpiring = 1, - ExpirationStatusValid = 2, + ExpirationStatusExpired = 0, + ExpirationStatusExpiring = 1, + ExpirationStatusValid = 2, }; ExpirationStatus expirationStatus(NSDate * _Nullable date); NSDate * _Nullable dateOrNil(NSDate * _Nullable value); diff --git a/ProvisionQL/Shared.m b/ProvisionQL/Shared.m index 23bb0e0..d3b8483 100644 --- a/ProvisionQL/Shared.m +++ b/ProvisionQL/Shared.m @@ -5,129 +5,129 @@ /// 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; + 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; } /// 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]) { - 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; + QuickLookInfo data = {}; + data.UTI = (__bridge NSString *)contentTypeUTI; + data.url = (__bridge NSURL *)url; + + if ([data.UTI isEqualToString:kDataType_ipa]) { + 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; } /// 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; + 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; } // MARK: - Plist /// 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; + 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; } /// 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 nil; + switch (meta.type) { + case FileTypeIPA: + case FileTypeArchive: + case FileTypeExtension: { + return asPlistOrNil(readPayloadFile(meta, @"Info.plist")); + } + case FileTypeProvision: + return nil; + } + return nil; } /// 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; - } - - 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); + 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; + } + + 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); } /// Read @c iTunesMetadata.plist if available NSDictionary * _Nullable readPlistItunes(QuickLookInfo meta) { - if (meta.type == FileTypeIPA) { - return asPlistOrNil([meta.zipFile unzipFile:@"iTunesMetadata.plist"]); - } - return nil; + if (meta.type == FileTypeIPA) { + return asPlistOrNil([meta.zipFile unzipFile:@"iTunesMetadata.plist"]); + } + return nil; } // MARK: - Other helper /// 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 (!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; } /// Ensures the value is of type @c NSDate inline NSDate * _Nullable dateOrNil(NSDate * _Nullable value) { - return [value isKindOfClass:[NSDate class]] ? value : nil; + return [value isKindOfClass:[NSDate class]] ? value : nil; } /// Ensures the value is of type @c NSArray inline NSArray * _Nullable arrayOrNil(NSArray * _Nullable value) { - return [value isKindOfClass:[NSArray class]] ? value : nil; + return [value isKindOfClass:[NSArray class]] ? value : nil; } diff --git a/ProvisionQL/ZipFile.m b/ProvisionQL/ZipFile.m index 4792088..ad0a24b 100644 --- a/ProvisionQL/ZipFile.m +++ b/ProvisionQL/ZipFile.m @@ -10,106 +10,106 @@ @interface ZipFile() @implementation ZipFile + (instancetype)open:(NSString *)path { - return [[self alloc] initWithFile:path]; + return [[self alloc] initWithFile:path]; } - (instancetype)initWithFile:(NSString *)path { - self = [super init]; - if (self) { - _pathToZipFile = path; - _centralDirectory = listZip(path); - } - return self; + 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; + 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) { + if (self.centralDirectory) { + ZipEntry *matchingFile = [self.centralDirectory zipEntryWithPath:filePath]; + if (!matchingFile) { #ifdef DEBUG - NSLog(@"[unzip] cant find '%@'", filePath); + 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; - } + // 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); + NSLog(@"[unzip] %@", matchingFile.filepath); #endif - NSData *data = unzipFileEntry(self.pathToZipFile, matchingFile); - if (data) { - return data; - } - } - // fallback to sys unzip - return [self sysUnzipFile:filePath]; + 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]]; + if (self.centralDirectory) { + NSData *data = [self unzipFile:filePath]; + if (data) { + NSString *outputPath = [targetDir stringByAppendingPathComponent:[filePath lastPathComponent]]; #ifdef DEBUG - NSLog(@"[unzip] write to %@", outputPath); + NSLog(@"[unzip] write to %@", outputPath); #endif - [data writeToFile:outputPath atomically:NO]; - return; - } - } - [self sysUnzipFile:filePath toDir:targetDir]; + [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]; - + 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:@" "]); + NSLog(@"[sys-call] unzip %@", [[task arguments] componentsJoinedByString:@" "]); #endif - - NSData *pipeData = [[[task standardOutput] fileHandleForReading] readDataToEndOfFile]; - [task waitUntilExit]; - if (pipeData.length == 0) { - return nil; - } - return pipeData; + + 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]; - + 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:@" "]); + NSLog(@"[sys-call] unzip %@", [[task arguments] componentsJoinedByString:@" "]); #endif - - [task waitUntilExit]; + + [task waitUntilExit]; } @end From a5418b4cf9bf5943728869d1280778f3efd9e709 Mon Sep 17 00:00:00 2001 From: relikd Date: Wed, 14 Feb 2024 12:18:07 +0100 Subject: [PATCH 25/35] ref: remove unused ZipEntry attributes --- ProvisionQL/3rd-party/pinch/ZipEntry.h | 33 +++++++------------------- ProvisionQL/3rd-party/pinch/ZipEntry.m | 4 ---- ProvisionQL/3rd-party/pinch/pinch.m | 1 - 3 files changed, 8 insertions(+), 30 deletions(-) diff --git a/ProvisionQL/3rd-party/pinch/ZipEntry.h b/ProvisionQL/3rd-party/pinch/ZipEntry.h index 77ae0e2..e415259 100755 --- a/ProvisionQL/3rd-party/pinch/ZipEntry.h +++ b/ProvisionQL/3rd-party/pinch/ZipEntry.h @@ -31,34 +31,17 @@ #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; +@interface ZipEntry : NSObject @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; - +@property (nonatomic, assign) unsigned int offset; +@property (nonatomic, assign) unsigned int method; +@property (nonatomic, assign) unsigned int sizeCompressed; +@property (nonatomic, assign) unsigned int sizeUncompressed; +@property (nonatomic, assign) unsigned int filenameLength; +@property (nonatomic, assign) unsigned int extraFieldLength; @end -@interface NSArray (ZipEntry) +@interface NSArray (ZipEntry) - (ZipEntry*)zipEntryWithPath:(NSString*)path; - @end diff --git a/ProvisionQL/3rd-party/pinch/ZipEntry.m b/ProvisionQL/3rd-party/pinch/ZipEntry.m index 769f487..6a6ae59 100755 --- a/ProvisionQL/3rd-party/pinch/ZipEntry.m +++ b/ProvisionQL/3rd-party/pinch/ZipEntry.m @@ -32,17 +32,13 @@ of this software and associated documentation files (the "Software"), to deal @implementation ZipEntry - -@synthesize url; @synthesize filepath; @synthesize offset; @synthesize method; @synthesize sizeCompressed; @synthesize sizeUncompressed; -@synthesize crc32; @synthesize filenameLength; @synthesize extraFieldLength; -@synthesize data; @end diff --git a/ProvisionQL/3rd-party/pinch/pinch.m b/ProvisionQL/3rd-party/pinch/pinch.m index a568f3b..03c8564 100755 --- a/ProvisionQL/3rd-party/pinch/pinch.m +++ b/ProvisionQL/3rd-party/pinch/pinch.m @@ -298,7 +298,6 @@ ZipRecordEnd findCentralDirectory(NSFileHandle *fp) { 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; From 2f55ccff08fb20f51be89e02563fc8f9f514b535 Mon Sep 17 00:00:00 2001 From: relikd Date: Wed, 14 Feb 2024 12:18:24 +0100 Subject: [PATCH 26/35] fix: empty plist data --- ProvisionQL/Shared.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProvisionQL/Shared.m b/ProvisionQL/Shared.m index d3b8483..6f7cc41 100644 --- a/ProvisionQL/Shared.m +++ b/ProvisionQL/Shared.m @@ -55,7 +55,7 @@ QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { /// Helper for optional chaining. NSDictionary * _Nullable asPlistOrNil(NSData * _Nullable data) { - if (!data) { return nil; } + if (!data.length) { return nil; } NSError *err; NSDictionary *dict = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:&err]; if (err) { From 9806f2086399c72be21f0ba9fe3e20e7b0e87c02 Mon Sep 17 00:00:00 2001 From: relikd Date: Wed, 14 Feb 2024 12:20:25 +0100 Subject: [PATCH 27/35] fix: select shortest filepath in unordered list --- ProvisionQL/3rd-party/pinch/ZipEntry.m | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ProvisionQL/3rd-party/pinch/ZipEntry.m b/ProvisionQL/3rd-party/pinch/ZipEntry.m index 6a6ae59..ca86436 100755 --- a/ProvisionQL/3rd-party/pinch/ZipEntry.m +++ b/ProvisionQL/3rd-party/pinch/ZipEntry.m @@ -40,13 +40,26 @@ @implementation ZipEntry @synthesize filenameLength; @synthesize extraFieldLength; +- (NSString *)description { + return [NSString stringWithFormat:@"", filepath, sizeUncompressed]; +} + @end @implementation NSArray (ZipEntry) +/// Find filename matching pattern and return shortest possible path (thus ignoring deeper nested files). - (ZipEntry*)zipEntryWithPath:(NSString*)path { NSPredicate *pred = [NSPredicate predicateWithFormat:@"filepath LIKE %@", path]; - return [self filteredArrayUsingPredicate:pred].firstObject; + NSUInteger shortest = 99999; + ZipEntry *bestMatch = nil; + for (ZipEntry *entry in [self filteredArrayUsingPredicate:pred]) { + if (shortest > entry.filepath.length) { + shortest = entry.filepath.length; + bestMatch = entry; + } + } + return bestMatch; } @end From 9f0b9311053b9b947cb05b49598c80acd4d9ece4 Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 26 Feb 2024 20:04:24 +0100 Subject: [PATCH 28/35] feat: try default names "Icon" and "icon" --- ProvisionQL/AppIcon.m | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/ProvisionQL/AppIcon.m b/ProvisionQL/AppIcon.m index f792989..f868377 100644 --- a/ProvisionQL/AppIcon.m +++ b/ProvisionQL/AppIcon.m @@ -67,33 +67,35 @@ - (NSImage * _Nonnull)extractImage:(NSDictionary * _Nullable)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) { + + // First, try if an image file with that name exists. + NSString *actualName = [self expandImageName:plistImgName ?: @"Icon"]; + if (!actualName) { + actualName = [self expandImageName:@"icon"]; + } + if (actualName) { #ifdef DEBUG - NSLog(@"[icon] using plist with key %@ and image file %@", plistImgName, actualName); + 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]]; + 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); + // Else: try Assets.car + @try { + NSImage *img = [self imageFromAssetsCar:plistImgName]; + if (img) { + return img; } -#endif + } @catch (NSException *exception) { + NSLog(@"ERROR: unknown private framework issue: %@", exception); } +#endif // Fallback to default icon NSURL *iconURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"defaultIcon" withExtension:@"png"]; From 1dc774cda3236564af8aa0e42c353c86ef2c2819 Mon Sep 17 00:00:00 2001 From: relikd Date: Tue, 27 Feb 2024 01:33:59 +0100 Subject: [PATCH 29/35] feat: use icon list to prevent "dead-links" --- ProvisionQL/AppIcon.m | 120 +++++++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 42 deletions(-) diff --git a/ProvisionQL/AppIcon.m b/ProvisionQL/AppIcon.m index f868377..8712163 100644 --- a/ProvisionQL/AppIcon.m +++ b/ProvisionQL/AppIcon.m @@ -63,19 +63,19 @@ - (NSImage * _Nonnull)extractImage:(NSDictionary * _Nullable)appPlist { } // Extract image name from app plist - NSString *plistImgName = [self iconNameFromPlist:appPlist]; + NSArray *plistImgNames = [self iconNamesFromPlist:appPlist]; #ifdef DEBUG - NSLog(@"[icon] icon name: %@", plistImgName); + NSLog(@"[icon] icon names in plist: %@", plistImgNames); #endif + // If no previous filename works (or empty), try default icon names + plistImgNames = [plistImgNames arrayByAddingObjectsFromArray:@[@"Icon", @"icon"]]; + // First, try if an image file with that name exists. - NSString *actualName = [self expandImageName:plistImgName ?: @"Icon"]; - if (!actualName) { - actualName = [self expandImageName:@"icon"]; - } + NSString *actualName = [self expandImageName:plistImgNames]; if (actualName) { #ifdef DEBUG - NSLog(@"[icon] using plist with key %@ and image file %@", plistImgName, actualName); + NSLog(@"[icon] using plist image file %@", actualName); #endif if (_meta.type == FileTypeIPA) { NSData *data = [_meta.zipFile unzipFile:[@"Payload/*.app/" stringByAppendingString:actualName]]; @@ -88,7 +88,7 @@ - (NSImage * _Nonnull)extractImage:(NSDictionary * _Nullable)appPlist { #ifdef CUI_ENABLED // Else: try Assets.car @try { - NSImage *img = [self imageFromAssetsCar:plistImgName]; + NSImage *img = [self imageFromAssetsCar:plistImgNames.firstObject]; if (img) { return img; } @@ -200,13 +200,13 @@ - (CUINamedImage * _Nullable)carFindHighestResolutionIcon:(NSArray * _Nonnull)iconNamesFromPlist:(NSDictionary *)appPlist { if (!appPlist) { appPlist = readPlistApp(_meta); } //Check for CFBundleIcons (since 5.0) - NSArray *icons = [self unpackNameListFromPlistDict:appPlist[@"CFBundleIcons"]]; + NSArray *icons = [self unpackNameListFromPlistDict:appPlist[@"CFBundleIcons"]]; if (!icons) { icons = [self unpackNameListFromPlistDict:appPlist[@"CFBundleIcons~ipad"]]; if (!icons) { @@ -216,45 +216,60 @@ - (NSString * _Nullable)iconNameFromPlist:(NSDictionary *)appPlist { 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 + NSString *icon = appPlist[@"CFBundleIconFile"]; // may be nil + return icon ? @[icon] : @[]; } } } } - return [self findHighestResolutionIconName:icons]; + return [self sortedByResolution: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; +- (NSString * _Nullable)expandImageName:(NSArray * _Nonnull)iconList { + NSMutableArray *matches = [NSMutableArray array]; 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:@"*"]; + return [[iconList firstObject] 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]]; + for (NSString *fileName in iconList) { + NSString *zipPath = [NSString stringWithFormat:@"Payload/*.app/%@*", fileName]; + for (ZipEntry *zip in [_meta.zipFile filesMatching:zipPath]) { + if (zip.sizeUncompressed > 0) { + [matches addObject:[zip.filepath lastPathComponent]]; + } + } + if (matches.count > 0) { + break; + } } - 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]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *appContents = [fileManager contentsOfDirectoryAtPath:basePath.path error:nil]; + for (NSString *fileName in iconList) { + for (NSString *file in appContents) { + if ([file hasPrefix:fileName]) { + NSString *fullPath = [basePath URLByAppendingPathComponent:file isDirectory:NO].path; + if ([fileManager attributesOfItemAtPath:fullPath error:nil].fileSize > 0) { + [matches addObject:file]; + } + } + } + if (matches.count > 0) { + break; + } + } } - if (matchingNames.count > 0) { - return [self findHighestResolutionIconName:matchingNames]; + if (matches.count > 0) { + return [self sortedByResolution:matches].firstObject; } return nil; } /// Deep select icons from plist key @c CFBundleIcons and @c CFBundleIcons~ipad -- (NSArray * _Nullable)unpackNameListFromPlistDict:(NSDictionary *)bundleDict { +- (NSArray * _Nullable)unpackNameListFromPlistDict:(NSDictionary *)bundleDict { if ([bundleDict isKindOfClass:[NSDictionary class]]) { NSDictionary *primaryDict = [bundleDict objectForKey:@"CFBundlePrimaryIcon"]; if ([primaryDict isKindOfClass:[NSDictionary class]]) { @@ -271,23 +286,44 @@ - (NSArray * _Nullable)unpackNameListFromPlistDict:(NSDictionary *)bundleDict { 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; - } +- (NSInteger)resolutionIndex:(NSString *)iconName { + const NSArray *RESOLUTION_ORDER = @[@"@3x", @"@2x", @"180", @"167", @"152", @"120"]; + for (int i = 0; i < RESOLUTION_ORDER.count; i++) { + if ([iconName containsString:RESOLUTION_ORDER[i]]) { + return i; } } - //If no one matches any pattern, just take last item - NSString *lastName = [icons lastObject]; - if ([[lastName lowercaseString] containsString:@"small"]) { - return [icons firstObject]; + if ([[iconName lowercaseString] containsString:@"small"]) { + return 99; } - return lastName; + return 50; } +- (NSArray *)sortedByResolution:(NSArray *)icons { + return [icons sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) { + NSInteger i1 = [self resolutionIndex:obj1]; + NSInteger i2 = [self resolutionIndex:obj2]; + return i1 < i2 ? NSOrderedAscending : i1 > i2? NSOrderedDescending : NSOrderedSame; + }]; +} + +/// 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 From 9559cd59753470e72288b94f5fd88e01155605a2 Mon Sep 17 00:00:00 2001 From: relikd Date: Tue, 27 Feb 2024 01:37:15 +0100 Subject: [PATCH 30/35] chore: remove comment --- ProvisionQL/AppIcon.m | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/ProvisionQL/AppIcon.m b/ProvisionQL/AppIcon.m index 8712163..c5789ef 100644 --- a/ProvisionQL/AppIcon.m +++ b/ProvisionQL/AppIcon.m @@ -286,6 +286,7 @@ - (NSString * _Nullable)expandImageName:(NSArray * _Nonnull)iconList return nil; } +/// @return lower index means higher resolution. - (NSInteger)resolutionIndex:(NSString *)iconName { const NSArray *RESOLUTION_ORDER = @[@"@3x", @"@2x", @"180", @"167", @"152", @"120"]; for (int i = 0; i < RESOLUTION_ORDER.count; i++) { @@ -299,6 +300,7 @@ - (NSInteger)resolutionIndex:(NSString *)iconName { return 50; } +/// Given a list of filenames, order them highest resolution first. - (NSArray *)sortedByResolution:(NSArray *)icons { return [icons sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) { NSInteger i1 = [self resolutionIndex:obj1]; @@ -307,23 +309,6 @@ - (NSInteger)resolutionIndex:(NSString *)iconName { }]; } -/// 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 From 46e63097ec1096bae750234a51ad488281b74d44 Mon Sep 17 00:00:00 2001 From: relikd Date: Tue, 27 Feb 2024 02:29:24 +0100 Subject: [PATCH 31/35] feat: support for icons in subdirectories --- ProvisionQL/AppIcon.m | 31 ++++++++++++++++--------------- ProvisionQL/Shared.m | 4 ++-- ProvisionQL/ZipFile.h | 2 +- ProvisionQL/ZipFile.m | 14 +++++++++----- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/ProvisionQL/AppIcon.m b/ProvisionQL/AppIcon.m index c5789ef..63be618 100644 --- a/ProvisionQL/AppIcon.m +++ b/ProvisionQL/AppIcon.m @@ -53,7 +53,7 @@ - (BOOL)canExtractImage { - (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"]; + NSData *data = [_meta.zipFile unzipFile:@"iTunesArtwork" isExactMatch:YES]; if (data) { #ifdef DEBUG NSLog(@"[icon] using iTunesArtwork."); @@ -78,11 +78,10 @@ - (NSImage * _Nonnull)extractImage:(NSDictionary * _Nullable)appPlist { NSLog(@"[icon] using plist image file %@", actualName); #endif if (_meta.type == FileTypeIPA) { - NSData *data = [_meta.zipFile unzipFile:[@"Payload/*.app/" stringByAppendingString:actualName]]; + NSData *data = [_meta.zipFile unzipFile:actualName isExactMatch:YES]; return [[NSImage alloc] initWithData:data]; } - NSURL *basePath = _meta.effectiveUrl ?: _meta.url; - return [[NSImage alloc] initWithContentsOfURL:[basePath URLByAppendingPathComponent:actualName]]; + return [[NSImage alloc] initWithContentsOfURL:[NSURL fileURLWithPath:actualName isDirectory:NO]]; } #ifdef CUI_ENABLED @@ -231,13 +230,13 @@ - (NSString * _Nullable)expandImageName:(NSArray * _Nonnull)iconList if (_meta.type == FileTypeIPA) { if (!_meta.zipFile) { // in case unzip in memory is not available, fallback to pattern matching with dynamic suffix - return [[iconList firstObject] stringByAppendingString:@"*"]; + return [NSString stringWithFormat:@"Payload/*.app/%@*", iconList.firstObject]; } - for (NSString *fileName in iconList) { - NSString *zipPath = [NSString stringWithFormat:@"Payload/*.app/%@*", fileName]; + for (NSString *iconPath in iconList) { + NSString *zipPath = [NSString stringWithFormat:@"Payload/*.app/%@*", iconPath]; for (ZipEntry *zip in [_meta.zipFile filesMatching:zipPath]) { if (zip.sizeUncompressed > 0) { - [matches addObject:[zip.filepath lastPathComponent]]; + [matches addObject:zip.filepath]; } } if (matches.count > 0) { @@ -247,13 +246,15 @@ - (NSString * _Nullable)expandImageName:(NSArray * _Nonnull)iconList } else if (_meta.type == FileTypeArchive || _meta.type == FileTypeExtension) { NSURL *basePath = _meta.effectiveUrl ?: _meta.url; NSFileManager *fileManager = [NSFileManager defaultManager]; - NSArray *appContents = [fileManager contentsOfDirectoryAtPath:basePath.path error:nil]; - for (NSString *fileName in iconList) { - for (NSString *file in appContents) { + for (NSString *iconPath in iconList) { + NSString *fileName = [iconPath lastPathComponent]; + NSString *basename = [basePath URLByAppendingPathComponent:iconPath isDirectory:NO].path; + NSString *parentDir = [basename stringByDeletingLastPathComponent]; + for (NSString *file in [fileManager contentsOfDirectoryAtPath:parentDir error:nil]) { if ([file hasPrefix:fileName]) { - NSString *fullPath = [basePath URLByAppendingPathComponent:file isDirectory:NO].path; + NSString *fullPath = [parentDir stringByAppendingPathComponent:file]; if ([fileManager attributesOfItemAtPath:fullPath error:nil].fileSize > 0) { - [matches addObject:file]; + [matches addObject:fullPath]; } } } @@ -303,8 +304,8 @@ - (NSInteger)resolutionIndex:(NSString *)iconName { /// Given a list of filenames, order them highest resolution first. - (NSArray *)sortedByResolution:(NSArray *)icons { return [icons sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) { - NSInteger i1 = [self resolutionIndex:obj1]; - NSInteger i2 = [self resolutionIndex:obj2]; + NSInteger i1 = [self resolutionIndex:[obj1 lastPathComponent]]; + NSInteger i2 = [self resolutionIndex:[obj2 lastPathComponent]]; return i1 < i2 ? NSOrderedAscending : i1 > i2? NSOrderedDescending : NSOrderedSame; }]; } diff --git a/ProvisionQL/Shared.m b/ProvisionQL/Shared.m index 6f7cc41..eb2ef9f 100644 --- a/ProvisionQL/Shared.m +++ b/ProvisionQL/Shared.m @@ -43,7 +43,7 @@ QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { /// 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 FileTypeIPA: return [meta.zipFile unzipFile:[@"Payload/*.app/" stringByAppendingString:filename] isExactMatch:NO]; case FileTypeArchive: return [NSData dataWithContentsOfURL:[meta.effectiveUrl URLByAppendingPathComponent:filename]]; case FileTypeExtension: return [NSData dataWithContentsOfURL:[meta.url URLByAppendingPathComponent:filename]]; case FileTypeProvision: return nil; @@ -106,7 +106,7 @@ QuickLookInfo initQLInfo(CFStringRef contentTypeUTI, CFURLRef url) { /// Read @c iTunesMetadata.plist if available NSDictionary * _Nullable readPlistItunes(QuickLookInfo meta) { if (meta.type == FileTypeIPA) { - return asPlistOrNil([meta.zipFile unzipFile:@"iTunesMetadata.plist"]); + return asPlistOrNil([meta.zipFile unzipFile:@"iTunesMetadata.plist" isExactMatch:YES]); } return nil; } diff --git a/ProvisionQL/ZipFile.h b/ProvisionQL/ZipFile.h index c178423..df0814e 100644 --- a/ProvisionQL/ZipFile.h +++ b/ProvisionQL/ZipFile.h @@ -5,7 +5,7 @@ NS_ASSUME_NONNULL_BEGIN @interface ZipFile : NSObject + (instancetype)open:(NSString *)path; -- (NSData * _Nullable)unzipFile:(NSString *)filePath; +- (NSData * _Nullable)unzipFile:(NSString *)filePath isExactMatch:(BOOL)exact; - (void)unzipFile:(NSString *)filePath toDir:(NSString *)targetDir; - (NSArray * _Nullable)filesMatching:(NSString * _Nonnull)path; @end diff --git a/ProvisionQL/ZipFile.m b/ProvisionQL/ZipFile.m index ad0a24b..e19fa5c 100644 --- a/ProvisionQL/ZipFile.m +++ b/ProvisionQL/ZipFile.m @@ -35,7 +35,7 @@ - (instancetype)initWithFile:(NSString *)path { /// Unzip file directly into memory. /// @param filePath File path inside zip file. -- (NSData * _Nullable)unzipFile:(NSString *)filePath { +- (NSData * _Nullable)unzipFile:(NSString *)filePath isExactMatch:(BOOL)exact { if (self.centralDirectory) { ZipEntry *matchingFile = [self.centralDirectory zipEntryWithPath:filePath]; if (!matchingFile) { @@ -56,7 +56,7 @@ - (NSData * _Nullable)unzipFile:(NSString *)filePath { } } // fallback to sys unzip - return [self sysUnzipFile:filePath]; + return [self sysUnzipFile:filePath isExactMatch:exact]; } /// Unzip file to filesystem. @@ -64,7 +64,7 @@ - (NSData * _Nullable)unzipFile:(NSString *)filePath { /// @param targetDir Directory in which to unzip the file. - (void)unzipFile:(NSString *)filePath toDir:(NSString *)targetDir { if (self.centralDirectory) { - NSData *data = [self unzipFile:filePath]; + NSData *data = [self unzipFile:filePath isExactMatch:NO]; if (data) { NSString *outputPath = [targetDir stringByAppendingPathComponent:[filePath lastPathComponent]]; #ifdef DEBUG @@ -80,11 +80,15 @@ - (void)unzipFile:(NSString *)filePath toDir:(NSString *)targetDir { // MARK: - fallback to sys call -- (NSData * _Nullable)sysUnzipFile:(NSString *)filePath { +- (NSData * _Nullable)sysUnzipFile:(NSString *)filePath isExactMatch:(BOOL)exact { NSTask *task = [NSTask new]; [task setLaunchPath:@"/usr/bin/unzip"]; [task setStandardOutput:[NSPipe pipe]]; - [task setArguments:@[@"-p", self.pathToZipFile, filePath, @"-x", @"*/*/*/*"]]; + if (exact) { + [task setArguments:@[@"-p", self.pathToZipFile, filePath]]; + } else { + [task setArguments:@[@"-p", self.pathToZipFile, filePath, @"-x", @"*/*/*/*"]]; + } [task launch]; #ifdef DEBUG From c179fb1471b7cc324146fe55ef652ebc00767f5c Mon Sep 17 00:00:00 2001 From: relikd Date: Sun, 17 Mar 2024 13:35:35 +0100 Subject: [PATCH 32/35] fix: avoid launch images --- ProvisionQL/AppIcon.m | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ProvisionQL/AppIcon.m b/ProvisionQL/AppIcon.m index 63be618..159e65a 100644 --- a/ProvisionQL/AppIcon.m +++ b/ProvisionQL/AppIcon.m @@ -289,16 +289,19 @@ - (NSString * _Nullable)expandImageName:(NSArray * _Nonnull)iconList /// @return lower index means higher resolution. - (NSInteger)resolutionIndex:(NSString *)iconName { + NSInteger penalty = 0; + if ([[iconName lowercaseString] containsString:@"small"] + || [[iconName lowercaseString] hasPrefix:@"default"]) // launch image + { + penalty = 10; + } const NSArray *RESOLUTION_ORDER = @[@"@3x", @"@2x", @"180", @"167", @"152", @"120"]; for (int i = 0; i < RESOLUTION_ORDER.count; i++) { if ([iconName containsString:RESOLUTION_ORDER[i]]) { - return i; + return i + penalty; } } - if ([[iconName lowercaseString] containsString:@"small"]) { - return 99; - } - return 50; + return 50 + penalty; } /// Given a list of filenames, order them highest resolution first. From dfd81d47ddce4a9db7c056862dbc253e02a891a4 Mon Sep 17 00:00:00 2001 From: relikd Date: Thu, 21 Mar 2024 18:13:22 +0100 Subject: [PATCH 33/35] fix: image.size incorrect if initialized from data --- ProvisionQL/AppIcon.m | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ProvisionQL/AppIcon.m b/ProvisionQL/AppIcon.m index 159e65a..772c711 100644 --- a/ProvisionQL/AppIcon.m +++ b/ProvisionQL/AppIcon.m @@ -372,9 +372,20 @@ + (NSBezierPath *)bezierPathWithIOS7RoundedRect:(NSRect)rect cornerRadius:(CGFlo @implementation NSImage (AppIcon) +/// Because some (PNG) image data will return weird float values +- (NSSize)bestImageSize { + NSInteger w = self.size.width; + NSInteger h = self.size.height; + for (NSImageRep * imageRep in [self representations]) { + w = MAX(w, [imageRep pixelsWide]); + h = MAX(h, [imageRep pixelsHigh]); + } + return NSMakeSize(w, h); +} + /// Apply rounded corners to image (iOS7 style) - (NSImage * _Nonnull)withRoundCorners { - NSSize existingSize = [self size]; + NSSize existingSize = [self bestImageSize]; NSImage *composedImage = [[NSImage alloc] initWithSize:existingSize]; [composedImage lockFocus]; @@ -385,7 +396,7 @@ - (NSImage * _Nonnull)withRoundCorners { [clipPath setWindingRule:NSWindingRuleEvenOdd]; [clipPath addClip]; - [self drawAtPoint:NSZeroPoint fromRect:NSMakeRect(0, 0, existingSize.width, existingSize.height) operation:NSCompositingOperationSourceOver fraction:1]; + [self drawInRect:imageFrame]; [composedImage unlockFocus]; return composedImage; } From c1d5440187a0941b3d100509fbc60e7a57a22573 Mon Sep 17 00:00:00 2001 From: relikd Date: Thu, 21 Mar 2024 18:16:18 +0100 Subject: [PATCH 34/35] fix: add more icon sizes (<=iOS4) --- ProvisionQL/AppIcon.m | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ProvisionQL/AppIcon.m b/ProvisionQL/AppIcon.m index 772c711..4c15118 100644 --- a/ProvisionQL/AppIcon.m +++ b/ProvisionQL/AppIcon.m @@ -293,9 +293,12 @@ - (NSInteger)resolutionIndex:(NSString *)iconName { if ([[iconName lowercaseString] containsString:@"small"] || [[iconName lowercaseString] hasPrefix:@"default"]) // launch image { - penalty = 10; + penalty = 20; } - const NSArray *RESOLUTION_ORDER = @[@"@3x", @"@2x", @"180", @"167", @"152", @"120"]; + const NSArray *RESOLUTION_ORDER = @[ + @"@3x", @"180", @"167", @"152", @"@2x", @"120", @"144", + @"114", @"87", @"80", @"76", @"72", @"58", @"57" + ]; for (int i = 0; i < RESOLUTION_ORDER.count; i++) { if ([iconName containsString:RESOLUTION_ORDER[i]]) { return i + penalty; From 44f2a9ffa5a5ab005d0b26ea0090b096fe0a30c3 Mon Sep 17 00:00:00 2001 From: relikd Date: Tue, 2 Apr 2024 18:40:43 +0200 Subject: [PATCH 35/35] doc: fix typo --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6e2a49..9ee34a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,11 @@ * 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 fixed-width size for preview of app-icon (consistency) * Fix `IconFlavor` attribute for thumbnail drawing in 10.15+ -* Fix prefer icons without "small" siffix +* Fix prefer icons without "small" suffix * Minor html template improvements -* Some refactoring to reduce duplicate code +* Some refactoring to reduce duplicate code ## Version 1.6.4