From 13a52cdc85eb524000548cbbb95e5d824279e768 Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 12 Feb 2024 01:04:34 +0100 Subject: [PATCH] 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