Add ZIP import support to the iOS version

This commit is contained in:
Lior Halphon 2024-08-18 19:19:01 +03:00
parent fb8508ea20
commit 2b2bb3569b
7 changed files with 326 additions and 87 deletions

View File

@ -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)

View File

@ -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 <NSURL *>*)urls
{
NSMutableArray<NSURL *> *validURLs = [NSMutableArray array];
NSMutableArray<NSString *> *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

View File

@ -24,5 +24,6 @@ typedef enum {
- (void)showAbout;
- (void)saveStateToFile:(NSString *)file;
- (bool)loadStateFromFile:(NSString *)file;
- (bool)handleOpenURLs:(NSArray <NSURL *> *)urls openInPlace:(bool)inPlace;
@property (nonatomic) GBRunMode runMode;
@end

View File

@ -13,6 +13,8 @@
#import "GBSettingsViewController.h"
#import "GBStatesViewController.h"
#import "GCExtendedGamepad+AllElements.h"
#import "GBZipReader.h"
#import <sys/stat.h>
#import <CoreMotion/CoreMotion.h>
#import <dlfcn.h>
@ -1147,23 +1149,117 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
GB_set_palette(&_gb, &_palette);
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
- (bool)handleOpenURLs:(NSArray <NSURL *> *)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<NSURL *> *validURLs = [NSMutableArray array];
NSMutableArray<NSString *> *skippedBasenames = [NSMutableArray array];
NSMutableArray<NSString *> *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<UIApplicationOpenURLOptionsKey,id> *)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

7
iOS/GBZipReader.h Normal file
View File

@ -0,0 +1,7 @@
#import <Foundation/Foundation.h>
@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

179
iOS/GBZipReader.m Normal file
View File

@ -0,0 +1,179 @@
#import "GBZipReader.h"
#import <compression.h>
#import <sys/mman.h>
#import <mach/vm_region.h>
#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

View File

@ -114,6 +114,14 @@
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>zip</string>
<key>LSItemContentTypes</key>
<array>
<string>com.pkware.zip-archive</string>
</array>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
@ -174,24 +182,24 @@
<integer>1</integer>
<integer>2</integer>
</array>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>GCSupportedGameControllers</key>
<array>
<dict>
<key>ProfileName</key>
<string>ExtendedGamepad</string>
</dict>
</array>
<key>GCSupportsControllerUserInteraction</key>
<true/>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>GCSupportedGameControllers</key>
<array>
<dict>
<key>ProfileName</key>
<string>ExtendedGamepad</string>
</dict>
</array>
<key>GCSupportsControllerUserInteraction</key>
<true/>
</dict>
</plist>