Path: blob/main/Natives/MinecraftResourceDownloadTask.m
589 views
#include <CommonCrypto/CommonDigest.h>12#import "authenticator/BaseAuthenticator.h"3#import "installer/modpack/ModpackAPI.h"4#import "AFNetworking.h"5#import "LauncherNavigationController.h"6#import "LauncherPreferences.h"7#import "MinecraftResourceDownloadTask.h"8#import "MinecraftResourceUtils.h"9#import "ios_uikit_bridge.h"10#import "utils.h"1112@interface MinecraftResourceDownloadTask ()13@property AFURLSessionManager* manager;14@end1516@implementation MinecraftResourceDownloadTask1718- (instancetype)init {19self = [super init];20// TODO: implement background download21NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];22configuration.timeoutIntervalForRequest = 86400;23//backgroundSessionConfigurationWithIdentifier:@"net.kdt.pojavlauncher.downloadtask"];24self.manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];25self.fileList = [NSMutableArray new];26self.progressList = [NSMutableArray new];27return self;28}2930// Add file to the queue31- (NSURLSessionDownloadTask *)createDownloadTask:(NSString *)url size:(NSUInteger)size sha:(NSString *)sha altName:(NSString *)altName toPath:(NSString *)path success:(void (^)())success {32BOOL fileExists = [NSFileManager.defaultManager fileExistsAtPath:path];33// logSuccess?34if (fileExists && [self checkSHA:sha forFile:path altName:altName]) {35if (success) success();36return nil;37} else if (![self checkAccessWithDialog:YES]) {38return nil;39}4041NSString *name = altName ?: path.lastPathComponent;42NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];43__block NSProgress *progress;44__block NSURLSessionDownloadTask *task = [self.manager downloadTaskWithRequest:request progress:nil45destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {46NSLog(@"[MCDL] Downloading %@", name);47progress = [self.manager downloadProgressForTask:task];48if (!size && task) {49[self addDownloadTaskToProgress:task size:response.expectedContentLength];50[self.fileList addObject:name];51}52[NSFileManager.defaultManager createDirectoryAtPath:path.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:nil];53[NSFileManager.defaultManager removeItemAtPath:path error:nil];54return [NSURL fileURLWithPath:path];55} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {56if (self.progress.cancelled) {57// Ignore any further errors58} else if (error != nil) {59[self finishDownloadWithError:error file:name];60} else if (![self checkSHA:sha forFile:path altName:altName]) {61[self finishDownloadWithErrorString:[NSString stringWithFormat:@"Failed to verify file %@: SHA1 mismatch", path.lastPathComponent]];62} else {63progress.totalUnitCount = progress.completedUnitCount;64if (success) success();65}66}];6768if (size && task) {69[self addDownloadTaskToProgress:task size:size];70[self.fileList addObject:name];71}7273return task;74}7576- (NSURLSessionDownloadTask *)createDownloadTask:(NSString *)url size:(NSUInteger)size sha:(NSString *)sha altName:(NSString *)altName toPath:(NSString *)path {77return [self createDownloadTask:url size:size sha:sha altName:altName toPath:path success:nil];78}7980- (void)addDownloadTaskToProgress:(NSURLSessionDownloadTask *)task size:(NSInteger)size {81NSProgress *progress = [self.manager downloadProgressForTask:task];82NSUInteger fileSize = size>0 ? size : 1;83progress.kind = NSProgressKindFile;84if (size > 0) {85progress.totalUnitCount = fileSize;86}87[self.progressList addObject:progress];88[self.progress addChild:progress withPendingUnitCount:fileSize];89self.progress.totalUnitCount += fileSize;90self.textProgress.totalUnitCount = self.progress.totalUnitCount;91}9293- (void)downloadVersionMetadata:(NSDictionary *)version success:(void (^)())success {94// Download base json95NSString *versionStr = version[@"id"];96if ([versionStr isEqualToString:@"latest-release"]) {97versionStr = getPrefObject(@"internal.latest_version.release");98} else if ([versionStr isEqualToString:@"latest-snapshot"]) {99versionStr = getPrefObject(@"internal.latest_version.snapshot");100}101102NSString *path = [NSString stringWithFormat:@"%1$s/versions/%2$@/%[email protected]", getenv("POJAV_GAME_DIR"), versionStr];103// Find it again to resolve latest-*104version = (id)[MinecraftResourceUtils findVersion:versionStr inList:remoteVersionList];105106void(^completionBlock)(void) = ^{107self.metadata = parseJSONFromFile(path);108if (self.metadata[@"NSErrorObject"]) {109[self finishDownloadWithErrorString:[self.metadata[@"NSErrorObject"] localizedDescription]];110return;111}112if (self.metadata[@"inheritsFrom"]) {113NSMutableDictionary *inheritsFromDict = parseJSONFromFile([NSString stringWithFormat:@"%1$s/versions/%2$@/%[email protected]", getenv("POJAV_GAME_DIR"), self.metadata[@"inheritsFrom"]]);114if (inheritsFromDict) {115[MinecraftResourceUtils processVersion:self.metadata inheritsFrom:inheritsFromDict];116self.metadata = inheritsFromDict;117}118}119[MinecraftResourceUtils tweakVersionJson:self.metadata];120success();121};122123if (!version) {124// This is likely local version, check if json exists and has inheritsFrom125NSMutableDictionary *json = parseJSONFromFile(path);126if (json[@"NSErrorObject"]) {127[self finishDownloadWithErrorString:[json[@"NSErrorObject"] localizedDescription]];128return;129} else if (json[@"inheritsFrom"]) {130version = (id)[MinecraftResourceUtils findVersion:json[@"inheritsFrom"] inList:remoteVersionList];131path = [NSString stringWithFormat:@"%1$s/versions/%2$@/%[email protected]", getenv("POJAV_GAME_DIR"), json[@"inheritsFrom"]];132} else {133completionBlock();134return;135}136}137138versionStr = version[@"id"];139NSString *url = version[@"url"];140NSString *sha = url.stringByDeletingLastPathComponent.lastPathComponent;141NSUInteger size = [version[@"size"] unsignedLongLongValue];142143NSURLSessionDownloadTask *task = [self createDownloadTask:url size:size sha:sha altName:nil toPath:path success:completionBlock];144[task resume];145}146147#pragma mark - Minecraft installation148149- (void)downloadAssetMetadataWithSuccess:(void (^)())success {150NSDictionary *assetIndex = self.metadata[@"assetIndex"];151if (!assetIndex) {152success();153return;154}155NSString *name = [NSString stringWithFormat:@"assets/indexes/%@.json", assetIndex[@"id"]];156NSString *path = [@(getenv("POJAV_GAME_DIR")) stringByAppendingPathComponent:name];157NSString *url = assetIndex[@"url"];158NSString *sha = url.stringByDeletingLastPathComponent.lastPathComponent;159NSUInteger size = [assetIndex[@"size"] unsignedLongLongValue];160NSURLSessionDownloadTask *task = [self createDownloadTask:url size:size sha:sha altName:name toPath:path success:^{161self.metadata[@"assetIndexObj"] = parseJSONFromFile(path);162success();163}];164[task resume];165}166167- (NSArray *)downloadClientLibraries {168NSMutableArray *tasks = [NSMutableArray new];169for (NSDictionary *library in self.metadata[@"libraries"]) {170NSString *name = library[@"name"];171172NSMutableDictionary *artifact = library[@"downloads"][@"artifact"];173if (artifact == nil && [name containsString:@":"]) {174NSLog(@"[MCDL] Unknown artifact object for %@, attempting to generate one", name);175artifact = [[NSMutableDictionary alloc] init];176NSString *prefix = library[@"url"] == nil ? @"https://libraries.minecraft.net/" : [library[@"url"] stringByReplacingOccurrencesOfString:@"http://" withString:@"https://"];177NSArray *libParts = [name componentsSeparatedByString:@":"];178artifact[@"path"] = [NSString stringWithFormat:@"%1$@/%2$@/%3$@/%2$@-%[email protected]", [libParts[0] stringByReplacingOccurrencesOfString:@"." withString:@"/"], libParts[1], libParts[2]];179artifact[@"url"] = [NSString stringWithFormat:@"%@%@", prefix, artifact[@"path"]];180artifact[@"sha1"] = library[@"checksums"][0];181}182183NSString *path = [NSString stringWithFormat:@"%s/libraries/%@", getenv("POJAV_GAME_DIR"), artifact[@"path"]];184NSString *sha = artifact[@"sha1"];185NSUInteger size = [artifact[@"size"] unsignedLongLongValue];186NSString *url = artifact[@"url"];187if ([library[@"skip"] boolValue]) {188NSLog(@"[MDCL] Skipped library %@", name);189continue;190}191192NSURLSessionDownloadTask *task = [self createDownloadTask:url size:size sha:sha altName:name toPath:path success:nil];193if (task) {194[tasks addObject:task];195} else if (self.progress.cancelled) {196return nil;197}198}199return tasks;200}201202- (NSArray *)downloadClientAssets {203NSMutableArray *tasks = [NSMutableArray new];204NSDictionary *assets = self.metadata[@"assetIndexObj"];205if (!assets) {206return @[];207}208for (NSString *name in assets[@"objects"]) {209NSDictionary *object = assets[@"objects"][name];210NSString *hash = object[@"hash"];211NSString *pathname = [NSString stringWithFormat:@"%@/%@", [hash substringToIndex:2], hash];212NSUInteger size = [object[@"size"] unsignedLongLongValue];213214NSString *path;215if ([assets[@"map_to_resources"] boolValue]) {216path = [NSString stringWithFormat:@"%s/resources/%@", getenv("POJAV_GAME_DIR"), name];217} else {218path = [NSString stringWithFormat:@"%s/assets/objects/%@", getenv("POJAV_GAME_DIR"), pathname];219}220221/* Special case for 1.19+222* Since 1.19-pre1, setting the window icon on macOS invokes ObjC.223* However, if an IOException occurs, it won't try to set.224* We skip downloading the icon file to workaround this. */225if ([name hasSuffix:@"/minecraft.icns"]) {226[NSFileManager.defaultManager removeItemAtPath:path error:nil];227continue;228}229230NSString *url = [NSString stringWithFormat:@"https://resources.download.minecraft.net/%@", pathname];231NSURLSessionDownloadTask *task = [self createDownloadTask:url size:size sha:hash altName:name toPath:path success:nil];232if (task) {233[tasks addObject:task];234} else if (self.progress.cancelled) {235return nil;236}237}238return tasks;239}240241- (void)downloadVersion:(NSDictionary *)version {242[self prepareForDownload];243[self downloadVersionMetadata:version success:^{244[self downloadAssetMetadataWithSuccess:^{245NSArray *libTasks = [self downloadClientLibraries];246NSArray *assetTasks = [self downloadClientAssets];247// Drop the 1 byte we set initially248self.progress.totalUnitCount--;249self.textProgress.totalUnitCount--;250if (self.progress.totalUnitCount == 0) {251// We have nothing to download, invoke completion observer252self.progress.totalUnitCount = 1;253self.progress.completedUnitCount = 1;254self.textProgress.totalUnitCount = 1;255self.textProgress.completedUnitCount = 1;256return;257}258[libTasks makeObjectsPerformSelector:@selector(resume)];259[assetTasks makeObjectsPerformSelector:@selector(resume)];260[self.metadata removeObjectForKey:@"assetIndexObj"];261}];262}];263}264265#pragma mark - Modpack installation266267- (void)downloadModpackFromAPI:(ModpackAPI *)api detail:(NSDictionary *)modDetail atIndex:(NSUInteger)selectedVersion {268[self prepareForDownload];269270NSString *url = modDetail[@"versionUrls"][selectedVersion];271NSUInteger size = [modDetail[@"versionSizes"][selectedVersion] unsignedLongLongValue];272NSString *sha = modDetail[@"versionHashes"][selectedVersion];273NSString *name = [[modDetail[@"title"] lowercaseString] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];274name = [name stringByReplacingOccurrencesOfString:@" " withString:@"_"];275NSString *packagePath = [NSTemporaryDirectory() stringByAppendingFormat:@"/%@.zip", name];276277NSURLSessionDownloadTask *task = [self createDownloadTask:url size:size sha:sha altName:nil toPath:packagePath success:^{278NSString *path = [NSString stringWithFormat:@"%s/custom_gamedir/%@", getenv("POJAV_GAME_DIR"), name];279[api downloader:self submitDownloadTasksFromPackage:packagePath toPath:path];280}];281[task resume];282}283284#pragma mark - Utilities285286- (void)prepareForDownload {287// Create a fake progress which is used to update completedUnitCount properly288// (completedUnitCount does not update unless subprogress completes)289self.textProgress = [NSProgress new];290self.textProgress.kind = NSProgressKindFile;291self.textProgress.fileOperationKind = NSProgressFileOperationKindDownloading;292self.textProgress.totalUnitCount = -1;293294self.progress = [NSProgress new];295// Push 1 byte so it won't accidentally finish after downloading assets index296self.progress.totalUnitCount = 1;297[self.fileList removeAllObjects];298[self.progressList removeAllObjects];299}300301- (void)finishDownloadWithErrorString:(NSString *)error {302[self.progress cancel];303[self.manager invalidateSessionCancelingTasks:YES resetSession:YES];304showDialog(localize(@"Error", nil), error);305self.handleError();306}307308- (void)finishDownloadWithError:(NSError *)error file:(NSString *)file {309NSString *errorStr = [NSString stringWithFormat:localize(@"launcher.mcl.error_download", NULL), file, error.localizedDescription];310NSLog(@"[MCDL] Error: %@ %@", errorStr, NSThread.callStackSymbols);311[self finishDownloadWithErrorString:errorStr];312}313314// Check if the account has permission to download315- (BOOL)checkAccessWithDialog:(BOOL)show {316// for now317BOOL accessible = [BaseAuthenticator.current.authData[@"username"] hasPrefix:@"Demo."] || BaseAuthenticator.current.authData[@"xboxGamertag"] != nil;318if (!accessible) {319[self.progress cancel];320if (show) {321[self finishDownloadWithErrorString:@"Minecraft can't be legally installed when logged in with a local account. Please switch to an online account to continue."];322}323}324return accessible;325}326327// Check SHA of the file328- (BOOL)checkSHAIgnorePref:(NSString *)sha forFile:(NSString *)path altName:(NSString *)altName logSuccess:(BOOL)logSuccess {329if (sha.length == 0) {330// When sha = skip, only check for file existence331BOOL existence = [NSFileManager.defaultManager fileExistsAtPath:path];332if (existence) {333NSLog(@"[MCDL] Warning: couldn't find SHA for %@, have to assume it's good.", path);334}335return existence;336}337338NSData *data = [NSData dataWithContentsOfFile:path];339if (data == nil) {340NSLog(@"[MCDL] SHA1 checker: file doesn't exist: %@", altName ? altName : path.lastPathComponent);341return NO;342}343344unsigned char digest[CC_SHA1_DIGEST_LENGTH];345CC_SHA1(data.bytes, (CC_LONG)data.length, digest);346NSMutableString *localSHA = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];347for(int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) {348[localSHA appendFormat:@"%02x", digest[i]];349}350351BOOL check = [sha isEqualToString:localSHA];352if (!check || (getPrefBool(@"general.debug_logging") && logSuccess)) {353NSLog(@"[MCDL] SHA1 %@ for %@%@",354(check ? @"passed" : @"failed"),355(altName ? altName : path.lastPathComponent),356(check ? @"" : [NSString stringWithFormat:@" (expected: %@, got: %@)", sha, localSHA]));357}358return check;359}360361- (BOOL)checkSHA:(NSString *)sha forFile:(NSString *)path altName:(NSString *)altName logSuccess:(BOOL)logSuccess {362if (getPrefBool(@"general.check_sha")) {363return [self checkSHAIgnorePref:sha forFile:path altName:altName logSuccess:logSuccess];364} else {365return [NSFileManager.defaultManager fileExistsAtPath:path];366}367}368369- (BOOL)checkSHA:(NSString *)sha forFile:(NSString *)path altName:(NSString *)altName {370return [self checkSHA:sha forFile:path altName:altName logSuccess:altName==nil];371}372373@end374375376