diff --git a/Makefile b/Makefile index fce2b1f..42fefcf 100644 --- a/Makefile +++ b/Makefile @@ -273,7 +273,7 @@ LDFLAGS += -arch arm64 OCFLAGS += -x objective-c -fobjc-arc -Wno-deprecated-declarations -isysroot $(SYSROOT) LDFLAGS += -miphoneos-version-min=$(IOS_MIN) -isysroot $(SYSROOT) IOS_INSTALLER_LDFLAGS := $(LDFLAGS) -lobjc -framework CoreServices -framework Foundation -LDFLAGS += -lobjc -framework UIKit -framework Foundation -framework CoreGraphics -framework Metal -framework MetalKit -framework AudioToolbox -framework AVFoundation -framework QuartzCore -framework CoreMotion -framework CoreVideo -framework CoreMedia -framework CoreImage -framework UserNotifications -framework GameController -weak_framework CoreHaptics -framework MobileCoreServices +LDFLAGS += -lobjc -framework UIKit -framework Foundation -framework CoreGraphics -framework Metal -framework MetalKit -framework AudioToolbox -framework AVFoundation -framework QuartzCore -framework CoreMotion -framework CoreVideo -framework CoreMedia -framework CoreImage -framework UserNotifications -framework GameController -weak_framework CoreHaptics -framework MobileCoreServices -lcompression CODESIGN := codesign -fs - else ifeq ($(PLATFORM),Darwin) diff --git a/iOS/GBLoadROMTableViewController.m b/iOS/GBLoadROMTableViewController.m index d0a63f0..894a750 100644 --- a/iOS/GBLoadROMTableViewController.m +++ b/iOS/GBLoadROMTableViewController.m @@ -122,6 +122,8 @@ } } + [extensions addObject:@"zip"]; + [self.presentingViewController dismissViewControllerAnimated:true completion:^{ UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"com.github.liji32.sameboy.gb", @"com.github.liji32.sameboy.gbc", @@ -161,61 +163,7 @@ - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { - NSMutableArray *validURLs = [NSMutableArray array]; - NSMutableArray *skippedBasenames = [NSMutableArray array]; - - for (NSURL *url in urls) { - if ([@[@"gb", @"gbc", @"isx"] containsObject:url.pathExtension.lowercaseString]) { - [validURLs addObject:url]; - } - else { - [skippedBasenames addObject:url.lastPathComponent]; - } - } - - if (skippedBasenames.count) { - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Unsupported Files" - message:[NSString stringWithFormat:@"Could not import the following files because they're not supported:\n%@", - [skippedBasenames componentsJoinedByString:@"\n"]] - preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:@"Close" - style:UIAlertActionStyleCancel - handler:^(UIAlertAction *action) { - [[NSUserDefaults standardUserDefaults] setBool:false forKey:@"GBShownUTIWarning"]; // Somebody might need a reminder - }]]; - [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:true completion:nil]; - urls = validURLs; - } - - if (urls.count == 1) { - NSURL *url = urls.firstObject; - NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent]; - if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) { - [GBROMManager sharedManager].currentROM = potentialROM; - } - else { - [url startAccessingSecurityScopedResource]; - [GBROMManager sharedManager].currentROM = - [[GBROMManager sharedManager] importROM:url.path - keepOriginal:true]; - [url stopAccessingSecurityScopedResource]; - } - [[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object:nil]; - } - else { - for (NSURL *url in urls) { - NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent]; - if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) { - // That's an already imported ROM - continue; - } - [url startAccessingSecurityScopedResource]; - [[GBROMManager sharedManager] importROM:url.path - keepOriginal:true]; - [url stopAccessingSecurityScopedResource]; - } - [(GBViewController *)[UIApplication sharedApplication].keyWindow.rootViewController openLibrary]; - } + [(GBViewController *)[UIApplication sharedApplication] handleOpenURLs:urls openInPlace:false]; } - (UIModalPresentationStyle)modalPresentationStyle diff --git a/iOS/GBViewController.h b/iOS/GBViewController.h index 3dd1b85..e7d9e5f 100644 --- a/iOS/GBViewController.h +++ b/iOS/GBViewController.h @@ -24,5 +24,6 @@ typedef enum { - (void)showAbout; - (void)saveStateToFile:(NSString *)file; - (bool)loadStateFromFile:(NSString *)file; +- (bool)handleOpenURLs:(NSArray *)urls openInPlace:(bool)inPlace; @property (nonatomic) GBRunMode runMode; @end diff --git a/iOS/GBViewController.m b/iOS/GBViewController.m index ef4819b..6c44b05 100644 --- a/iOS/GBViewController.m +++ b/iOS/GBViewController.m @@ -13,6 +13,8 @@ #import "GBSettingsViewController.h" #import "GBStatesViewController.h" #import "GCExtendedGamepad+AllElements.h" +#import "GBZipReader.h" +#import #import #import @@ -1147,23 +1149,117 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response GB_set_palette(&_gb, &_palette); } -- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options +- (bool)handleOpenURLs:(NSArray *)urls openInPlace:(bool)inPlace { - [self stop]; - NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent]; - if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) { - [GBROMManager sharedManager].currentROM = potentialROM; + NSMutableArray *validURLs = [NSMutableArray array]; + NSMutableArray *skippedBasenames = [NSMutableArray array]; + NSMutableArray *unusedZips = [NSMutableArray array]; + NSString *tempDir = NSTemporaryDirectory(); + + __block unsigned tempIndex = 0; + for (NSURL *url in urls) { + if ([url.pathExtension.lowercaseString isEqualToString:@"zip"]) { + GBZipReader *reader = [[GBZipReader alloc] initWithPath:url.path]; + if (!reader) { + [skippedBasenames addObject:url.lastPathComponent]; + continue; + } + __block bool used = false; + [reader iterateFiles:^bool(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)) { + if ([@[@"gb", @"gbc", @"isx"] containsObject:filename.pathExtension.lowercaseString] && uncompressedSize <= 32 * 1024 * 1024) { + tempIndex++; + NSString *subDir = [tempDir stringByAppendingFormat:@"/%u", tempIndex]; + mkdir(subDir.UTF8String, 0755); + NSString *dest = [subDir stringByAppendingPathComponent:filename.lastPathComponent]; + if (writeToPath(dest)) { + [validURLs addObject:[NSURL fileURLWithPath:dest]]; + used = true; + } + } + return true; + }]; + if (!used) { + [unusedZips addObject:url.lastPathComponent]; + } + } + else if ([@[@"gb", @"gbc", @"isx"] containsObject:url.pathExtension.lowercaseString]) { + [validURLs addObject:url]; + } + else { + [skippedBasenames addObject:url.lastPathComponent]; + } } - else { - [url startAccessingSecurityScopedResource]; - [GBROMManager sharedManager].currentROM = + + if (unusedZips.count) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"No ROMs in archive" + message:[NSString stringWithFormat:@"Could not find any Game Boy ROM files in the following archives:\n%@", + [unusedZips componentsJoinedByString:@"\n"]] + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:nil]]; + [self stop]; + [self presentViewController:alert animated:true completion:nil]; + } + + if (skippedBasenames.count) { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Unsupported Files" + message:[NSString stringWithFormat:@"Could not import the following files because they're not supported:\n%@", + [skippedBasenames componentsJoinedByString:@"\n"]] + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + [[NSUserDefaults standardUserDefaults] setBool:false forKey:@"GBShownUTIWarning"]; // Somebody might need a reminder + }]]; + [self stop]; + [self presentViewController:alert animated:true completion:nil]; + } + + if (validURLs.count == 1 && urls.count == 1) { + NSURL *url = validURLs.firstObject; + NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent]; + if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) { + [GBROMManager sharedManager].currentROM = potentialROM; + } + else { + [url startAccessingSecurityScopedResource]; + [GBROMManager sharedManager].currentROM = [[GBROMManager sharedManager] importROM:url.path - keepOriginal:[options[UIApplicationOpenURLOptionsOpenInPlaceKey] boolValue]]; + keepOriginal:![url.path hasPrefix:tempDir] && !inPlace]; + [url stopAccessingSecurityScopedResource]; + } + [[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object:nil]; + return true; + } + for (NSURL *url in validURLs) { + NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent]; + if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) { + // That's an already imported ROM + continue; + } + [url startAccessingSecurityScopedResource]; + [[GBROMManager sharedManager] importROM:url.path + keepOriginal:![url.path hasPrefix:tempDir] && !inPlace]; [url stopAccessingSecurityScopedResource]; } - [self loadROM]; - [self start]; - return [GBROMManager sharedManager].currentROM != nil; + [self stop]; + [self openLibrary]; + + return validURLs.count; +} + +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options +{ + NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent]; + if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) { + [self stop]; + [GBROMManager sharedManager].currentROM = potentialROM; + [self loadROM]; + [self start]; + return [GBROMManager sharedManager].currentROM != nil; + } + return [self handleOpenURLs:@[url] openInPlace:[options[UIApplicationOpenURLOptionsOpenInPlaceKey] boolValue]]; } - (void)setRunMode:(GBRunMode)runMode ignoreDynamicSpeed:(bool)ignoreDynamicSpeed diff --git a/iOS/GBZipReader.h b/iOS/GBZipReader.h new file mode 100644 index 0000000..3b7eb0a --- /dev/null +++ b/iOS/GBZipReader.h @@ -0,0 +1,7 @@ +#import + +@interface GBZipReader : NSObject +- (instancetype)initWithBuffer:(const void *)buffer size:(size_t)size free:(void (^)(void))callback; +- (instancetype)initWithPath:(NSString *)path; +- (void)iterateFiles:(bool (^)(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)))callback; +@end diff --git a/iOS/GBZipReader.m b/iOS/GBZipReader.m new file mode 100644 index 0000000..88f86bc --- /dev/null +++ b/iOS/GBZipReader.m @@ -0,0 +1,179 @@ +#import "GBZipReader.h" +#import +#import +#import +#pragma clang diagnostic ignored "-Wimplicit-retain-self" + +@implementation GBZipReader +{ + void (^_freeCallback)(void); + const void *_buffer; + size_t _size; + + const struct __attribute__((packed)) { + uint32_t magic; + uint8_t skip[6]; + uint16_t fileCount; + uint32_t cdSize; + uint32_t cdOffset; + uint16_t commentSize; + } *_eocd; +} + +- (instancetype)initWithBuffer:(const void *)buffer size:(size_t)size free:(void (^)(void))callback +{ + self = [super init]; + if (!self) return nil; + + _buffer = buffer; + _size = size; + _freeCallback = callback; + + if (_size < sizeof(*_eocd)) return nil; + + for (unsigned i = 0; i < 0x10000; i++) { + _eocd = (void *)((uint8_t *)buffer + size - sizeof(*_eocd) - i); + if ((void *)_eocd < buffer) return nil; + if (_eocd->magic == htonl('PK\05\06')) { + break; + } + } + + return self; +} + +- (instancetype)initWithPath:(NSString *)path +{ + int fd = open(path.UTF8String, O_RDONLY); + if (fd < 0) return nil; + size_t size = lseek(fd, 0, SEEK_END); + size_t alignedSize = (size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); + void *mapping = mmap(NULL, alignedSize, PROT_READ, MAP_FILE | MAP_PRIVATE, fd, 0); + close(fd); + if (!mapping) return nil; + + return [self initWithBuffer:mapping size:size free:^{ + munmap(mapping, alignedSize); + }]; +} + +- (void)iterateFiles:(bool (^)(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)))callback { + const struct __attribute__((packed)) { + uint32_t magic; + uint8_t skip[6]; + uint16_t compressionMethod; + uint8_t skip2[8]; + uint32_t compressedSize; + uint32_t uncompressedSize; + uint16_t nameLength; + uint16_t extraLength; + uint16_t commentLength; + uint8_t skip3[8]; + uint32_t localHeaderOffset; + char name[0]; + } *entry = (void *)((uint8_t *)_buffer + _eocd->cdOffset); + for (unsigned i = _eocd->fileCount; i--;) { + if ((uint8_t *)entry + sizeof(*entry) - (uint8_t *)_buffer >= _size) return; + if (entry->magic != htonl('PK\01\02')) return; + + typeof(entry) next = (void *)((uint8_t *)entry + sizeof(*entry) + + entry->nameLength + entry->extraLength + entry->commentLength); + if ((uint8_t *)next - (uint8_t *)_buffer >= _size) return; + + + bool (^getData)(void *) = ^bool(void *output) { + // Directory, no data + if (entry->name[entry->nameLength - 1] == '/') return false; + + if (entry->uncompressedSize == 0xffffffff || entry->compressedSize == 0xffffffff) { + // ZIP64 + return false; + } + + const struct __attribute__((packed)) { + uint32_t magic; + uint8_t skip[4]; + uint16_t compressionMethod; + uint8_t skip2[8]; + uint32_t compressedSize; + uint32_t uncompressedSize; + uint16_t nameLength; + uint16_t extraLength; + char name[0]; + } *localEntry = (void *)((uint8_t *)_buffer + entry->localHeaderOffset); + + if ((uint8_t *)localEntry + sizeof(*localEntry) - (uint8_t *)_buffer >= _size) return nil; + if ((uint8_t *)localEntry + sizeof(*localEntry) + + localEntry->nameLength + localEntry->extraLength + + entry->compressedSize - (uint8_t *)_buffer >= _size) { + return false; + } + + if (localEntry->magic != htonl('PK\03\04')) return nil; + if (entry->uncompressedSize != localEntry->uncompressedSize) return nil; + + const void *dataStart = &localEntry->name[localEntry->nameLength + localEntry->extraLength]; + if (localEntry->compressionMethod == 0) { + if (localEntry->uncompressedSize != entry->compressedSize) return false; + memcpy(output, dataStart, localEntry->uncompressedSize); + return true; + } + else if (localEntry->compressionMethod != 8) { + // Unsupported compression + return false; + } + if (compression_decode_buffer(output, localEntry->uncompressedSize, + dataStart, entry->compressedSize, + NULL, COMPRESSION_ZLIB) != localEntry->uncompressedSize) { + return false; + } + return true; + }; + + bool (^writeToPath)(NSString *) = ^bool(NSString *path) { + int fd = open(path.UTF8String, O_CREAT | O_RDWR, 0644); + if (!fd) return false; + if (ftruncate(fd, entry->uncompressedSize) != 0) { + close(fd); + unlink(path.UTF8String); + return false; + } + size_t alignedSize = (entry->uncompressedSize + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); + void *mapping = mmap(NULL, alignedSize, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0); + close(fd); + if (!mapping) { + unlink(path.UTF8String); + return false; + } + + bool ret = getData(mapping); + if (!ret) { + unlink(path.UTF8String); + } + munmap(mapping, alignedSize); + + return ret; + }; + + + if (!callback([[NSString alloc] initWithBytes:entry->name + length:entry->nameLength + encoding:NSUTF8StringEncoding], + entry->uncompressedSize, + getData, + writeToPath)) { + return; + } + + entry = next; + } +} + +- (void)dealloc +{ + if (_freeCallback) { + _freeCallback(); + } +} + +@end diff --git a/iOS/Info.plist b/iOS/Info.plist index f988a81..33c46cc 100644 --- a/iOS/Info.plist +++ b/iOS/Info.plist @@ -114,6 +114,14 @@ LSHandlerRank Owner + + CFBundleTypeName + zip + LSItemContentTypes + + com.pkware.zip-archive + + UTExportedTypeDeclarations @@ -174,24 +182,24 @@ 1 2 - CFBundleSupportedPlatforms - - iPhoneOS - - UIRequiredDeviceCapabilities - - arm64 - - UIRequiresFullScreen - - GCSupportedGameControllers - - - ProfileName - ExtendedGamepad - - - GCSupportsControllerUserInteraction - + CFBundleSupportedPlatforms + + iPhoneOS + + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + GCSupportedGameControllers + + + ProfileName + ExtendedGamepad + + + GCSupportsControllerUserInteraction +