Path: blob/main/Natives/LauncherPrefManageJREViewController.m
589 views
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>1#import "WFWorkflowProgressView.h"2#import "LauncherNavigationController.h"3#import "LauncherPreferences.h"4#import "LauncherPrefManageJREViewController.h"5#import "NSFileManager+NRFileManager.h"6#import "UIKit+hook.h"7#import "ios_uikit_bridge.h"8#import "utils.h"910#include <dlfcn.h>11#include <lzma.h>12#include <objc/runtime.h>1314// 0 is reserved for default pickers15// INT_MAX is reserved for invalid runtimes16#define DEFAULT_JRE 017#define INVALID_JRE INT_MAX1819// https://www.gnu.org/software/tar/manual/html_node/Standard.html20typedef struct {21char name[100];22char mode[8];23char unused1[8+8];24char size[12];25char unused2[12+8];26char typeflag;27char linkname[100];28char unused3[6+2+32+32+8+8+155+12];29} TarHeader;3031static WFWorkflowProgressView* currentProgressView;3233@interface LauncherPrefManageJREViewController ()<UIContextMenuInteractionDelegate, UIDocumentPickerDelegate>34@property(nonatomic) NSMutableDictionary<NSNumber *, NSMutableArray *> *javaRuntimes;35@property(nonatomic) NSMutableArray<NSNumber *> *sortedJavaVersions;36@property(nonatomic) NSArray<NSString *> *selectedRTTags;37@property(nonatomic) NSMutableDictionary<NSString *, NSString *> *selectedRuntimes;38@property(nonatomic) UIMenu* currentMenu;39@property(nonatomic, weak) NSIndexPath* installingIndexPath;40@end4142@implementation LauncherPrefManageJREViewController4344+ (LauncherPrefManageJREViewController *)currentInstance {45UISplitViewController *splitVC = (id)currentVC();46LauncherNavigationController *nav = (id)splitVC.viewControllers[1];47if (![nav.topViewController isKindOfClass:LauncherPrefManageJREViewController.class]) {48return nil;49}50return (id)nav.topViewController;51}5253- (void)viewDidLoad54{55[super viewDidLoad];56[self setTitle:localize(@"preference.title.manage_runtime", nil)];5758self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage systemImageNamed:@"plus"] style:UIBarButtonItemStylePlain target:self action:@selector(actionImportRuntime)];5960self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];61self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;6263self.javaRuntimes = @{64@(DEFAULT_JRE): @[@"preference.manage_runtime.default.1165", @"preference.manage_runtime.default.117", @"launcher.menu.execute_jar"]65}.mutableCopy;66self.sortedJavaVersions = @[@(DEFAULT_JRE)].mutableCopy;6768self.selectedRTTags = @[@"1_16_5_older", @"1_17_newer", @"execute_jar"];69self.selectedRuntimes = getPrefObject(@"java.java_homes");7071NSString *internalPath = [NSString stringWithFormat:@"%@/java_runtimes", NSBundle.mainBundle.bundlePath];72NSString *externalPath = [NSString stringWithFormat:@"%s/java_runtimes", getenv("POJAV_HOME")];73[self listJREInPath:internalPath markInternal:YES];74[self listJREInPath:externalPath markInternal:NO];7576// Load WFWorkflowProgressView77dlopen("/System/Library/PrivateFrameworks/WorkflowUIServices.framework/WorkflowUIServices", RTLD_GLOBAL);78}7980+ (void)actionCancelImportRuntime {81UISplitViewController *splitVC = (id)currentVC();82LauncherNavigationController *nav = (id)splitVC.viewControllers[1];83[nav.progressViewMain.observedProgress cancel];84}8586- (void)actionImportRuntime {87UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc]88initForOpeningContentTypes:@[[UTType typeWithMIMEType:@"application/x-xz"]]];89documentPicker.delegate = self;90documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;91[self presentViewController:documentPicker animated:YES completion:nil];92}9394- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url {95LauncherNavigationController *nav = (id)self.navigationController;96[nav setInteractionEnabled:NO forDownloading:NO];9798[url startAccessingSecurityScopedResource];99NSUInteger xzSize = [NSFileManager.defaultManager attributesOfItemAtPath:url.path error:nil].fileSize;100101NSProgress *totalProgress = [NSProgress progressWithTotalUnitCount:xzSize];102NSProgress *fileProgress = [NSProgress progressWithTotalUnitCount:0];103nav.progressViewMain.observedProgress = totalProgress;104nav.progressViewSub.observedProgress = fileProgress;105106dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{107108NSString *outPath = [NSString stringWithFormat:@"%s/java_runtimes/%@", getenv("POJAV_HOME"),109[url.path substringToIndex:url.path.length-7].lastPathComponent];110NSString *error = [LauncherPrefManageJREViewController extractTarXZ:url.path111to:outPath progress:totalProgress fileProgress:fileProgress112fileCallback:^(NSString* name) {113NSString *completedSize = [NSByteCountFormatter stringFromByteCount:fileProgress.completedUnitCount countStyle:NSByteCountFormatterCountStyleMemory];114NSString *totalSize = [NSByteCountFormatter stringFromByteCount:fileProgress.totalUnitCount countStyle:NSByteCountFormatterCountStyleMemory];115nav.progressText.text = [NSString stringWithFormat:@"(%@ / %@) %@", completedSize, totalSize, name];116currentProgressView.fractionCompleted = totalProgress.fractionCompleted;117}];118[url stopAccessingSecurityScopedResource];119120if (error) {121showDialog(localize(@"Error", nil), error);122}123124dispatch_async(dispatch_get_main_queue(), ^{125LauncherPrefManageJREViewController *vc = LauncherPrefManageJREViewController.currentInstance;126currentProgressView = nil;127128if ((error || totalProgress.cancelled) && vc.installingIndexPath) {129[vc removeRuntimeAtIndexPath:vc.installingIndexPath];130} else {131NSNumber *version = self.sortedJavaVersions[vc.installingIndexPath.section];132NSString *name = self.javaRuntimes[version][vc.installingIndexPath.row];133objc_setAssociatedObject(name, @"installing", @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC);134}135vc.installingIndexPath = nil;136[vc.tableView reloadData];137138[nav setInteractionEnabled:YES forDownloading:NO];139nav.progressViewMain.observedProgress = nil;140nav.progressViewSub.observedProgress = nil;141nav.progressText.text = @"";142});143});144}145146#pragma mark Table view147148- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {149return self.sortedJavaVersions.count;150}151152- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {153NSNumber *version = self.sortedJavaVersions[section];154switch (self.sortedJavaVersions[section].intValue) {155case DEFAULT_JRE: return localize(@"preference.manage_runtime.header.default", nil);156case INVALID_JRE: return localize(@"preference.manage_runtime.header.invalid", nil);157default: return [NSString stringWithFormat:@"Java %@", self.sortedJavaVersions[section]];158}159}160161- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {162switch (self.sortedJavaVersions[section].intValue) {163case DEFAULT_JRE: return localize(@"preference.manage_runtime.footer.default", nil);164case 8: return localize(@"preference.manage_runtime.footer.java8", nil);165case 17: return localize(@"preference.manage_runtime.footer.java17", nil);166default: return nil;167}168}169170- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {171return self.javaRuntimes[self.sortedJavaVersions[section]].count;172}173174- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath175{176switch (indexPath.section) {177case 0: return [self tableView:tableView cellForDefaultRowAtIndexPath:indexPath];178default: return [self tableView:tableView cellForRuntimeRowAtIndexPath:indexPath];179}180}181182- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForDefaultRowAtIndexPath:(NSIndexPath *)indexPath183{184UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DefaultCell"];185if (cell == nil) {186cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"DefaultCell"];187cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;188}189cell.textLabel.text = localize(self.javaRuntimes[@DEFAULT_JRE][indexPath.row], nil);190cell.detailTextLabel.text = [NSString stringWithFormat:@"Java %@",191((NSDictionary *)self.selectedRuntimes[@"0"])[self.selectedRTTags[indexPath.row]]];192return cell;193}194195- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRuntimeRowAtIndexPath:(NSIndexPath *)indexPath {196UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"RTCell"];197if (cell == nil) {198cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"RTCell"];199}200NSNumber *version = self.sortedJavaVersions[indexPath.section];201NSString *name = self.javaRuntimes[version][indexPath.row];202BOOL isInternal = [objc_getAssociatedObject(name, @"internalJRE") boolValue];203BOOL isInstalling = [objc_getAssociatedObject(name, @"installing") boolValue];204if (isInternal) {205cell.textLabel.text = [NSString stringWithFormat:@"[Internal] %@", name];206} else {207cell.textLabel.text = name;208}209210if (isInstalling) {211self.installingIndexPath = indexPath;212213LauncherNavigationController *nav = (id)self.navigationController;214if (!currentProgressView) {215currentProgressView = [[NSClassFromString(@"WFWorkflowProgressView") alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];216currentProgressView.resolvedTintColor = self.view.tintColor;217[currentProgressView addTarget:LauncherPrefManageJREViewController.class218action:@selector(actionCancelImportRuntime) forControlEvents:UIControlEventPrimaryActionTriggered];219}220221cell.accessoryView = currentProgressView;222} else {223cell.accessoryView = nil;224}225226// Set checkmark; with internal runtime it's a bit tricky to check227if ([self.selectedRuntimes[version.stringValue] isEqualToString:name] ||228(isInternal && [self.selectedRuntimes[version.stringValue] isEqualToString:@"internal"])) {229cell.accessoryType = UITableViewCellAccessoryCheckmark;230} else {231cell.accessoryType = UITableViewCellAccessoryNone;232}233234// Display size235dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{236unsigned long long folderSize = 0;237NSString *directory = [NSString stringWithFormat:@"%@/java_runtimes/%@",238isInternal ? NSBundle.mainBundle.bundlePath : @(getenv("POJAV_HOME")),239name];240[NSFileManager.defaultManager nr_getAllocatedSize:&folderSize ofDirectoryAtURL:[NSURL fileURLWithPath:directory] error:nil];241dispatch_async(dispatch_get_main_queue(), ^{242cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ - %@",243[self javaVersionForPath:directory],244[NSByteCountFormatter stringFromByteCount:folderSize countStyle:NSByteCountFormatterCountStyleMemory]];245});246});247248return cell;249}250251- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {252[tableView deselectRowAtIndexPath:indexPath animated:NO];253if (indexPath.section == 0) {254[self tableView:tableView openPickerAtIndexPath:indexPath255minVersion:(indexPath.row==1 ? 17 : 8)];256return;257} else if (self.sortedJavaVersions[indexPath.section].intValue == INVALID_JRE) {258// TODO: do something, like alert explaining that the runtime has missing files259return;260}261262// Update preference263NSNumber *version = self.sortedJavaVersions[indexPath.section];264NSString *name = self.javaRuntimes[version][indexPath.row];265BOOL isInternal = [objc_getAssociatedObject(name, @"internalJRE") boolValue];266if (isInternal) {267self.selectedRuntimes[version.stringValue] = @"internal";268} else {269self.selectedRuntimes[version.stringValue] = name;270}271setPrefObject(@"java.java_homes", self.selectedRuntimes);272273// Update checkmark274NSInteger runtimeCount = [self tableView:tableView numberOfRowsInSection:indexPath.section];275for (int i = 0; i < runtimeCount; i++) {276UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:indexPath.section]];277if (i == indexPath.row) {278cell.accessoryType = UITableViewCellAccessoryCheckmark;279} else {280cell.accessoryType = UITableViewCellAccessoryNone;281}282}283}284285#pragma mark Context Menu configuration286287- (void)tableView:(UITableView *)tableView openPickerAtIndexPath:(NSIndexPath *)indexPath minVersion:(NSInteger)minVer {288UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];289//NSDictionary *item = self.prefContents[indexPath.section][indexPath.row];290291NSMutableArray *menuItems = [NSMutableArray new];292for (int i = 1; i < self.sortedJavaVersions.count; i++) {293if (self.sortedJavaVersions[i].intValue < minVer ||294self.sortedJavaVersions[i].intValue == INVALID_JRE) {295continue;296}297NSString *version = [self tableView:tableView titleForHeaderInSection:i];298[menuItems addObject:[UIAction299actionWithTitle:version300image:nil301identifier:nil302handler:^(UIAction *action) {303cell.detailTextLabel.text = version;304((NSMutableDictionary *)self.selectedRuntimes[@"0"])[self.selectedRTTags[indexPath.row]] = self.sortedJavaVersions[i].stringValue;305setPrefObject(@"java.java_homes", self.selectedRuntimes);306}]];307}308309cell.detailTextLabel.interactions = [NSArray new];310311if (menuItems.count == 0) {312[menuItems addObject:[UIAction313actionWithTitle:localize(@"None", nil)314image:nil315identifier:nil316handler:^(UIAction *action){}]];317}318319self.currentMenu = [UIMenu menuWithTitle:cell.textLabel.text children:menuItems];320UIContextMenuInteraction *interaction = [[UIContextMenuInteraction alloc] initWithDelegate:self];321[cell.detailTextLabel addInteraction:interaction];322[interaction _presentMenuAtLocation:CGPointZero];323}324325- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction configurationForMenuAtLocation:(CGPoint)location326{327return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {328return self.currentMenu;329}];330}331332- (_UIContextMenuStyle *)_contextMenuInteraction:(UIContextMenuInteraction *)interaction333styleForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration334{335_UIContextMenuStyle *style = [_UIContextMenuStyle defaultStyle];336style.preferredLayout = 3; // _UIContextMenuLayoutCompactMenu337return style;338}339340- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath341{342if (editingStyle != UITableViewCellEditingStyleDelete) return;343344UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];345NSString *title = localize(@"preference.title.confirm", nil);346NSString *message = [NSString stringWithFormat:localize(@"preference.title.confirm.delete_runtime", nil), cell.textLabel.text];347UIAlertController *confirmAlert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleActionSheet];348confirmAlert.popoverPresentationController.sourceView = cell;349confirmAlert.popoverPresentationController.sourceRect = cell.bounds;350UIAlertAction *ok = [UIAlertAction actionWithTitle:localize(@"OK", nil) style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {351NSError *error;352NSString *directory = [NSString stringWithFormat:@"%s/java_runtimes/%@", getenv("POJAV_HOME"), cell.textLabel.text];353[NSFileManager.defaultManager removeItemAtPath:directory error:&error];354if(!error) {355[self removeRuntimeAtIndexPath:indexPath];356[self.tableView reloadData];357} else {358showDialog(localize(@"Error", nil), error.localizedDescription);359}360}];361UIAlertAction *cancel = [UIAlertAction actionWithTitle:localize(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil];362[confirmAlert addAction:cancel];363[confirmAlert addAction:ok];364[self presentViewController:confirmAlert animated:YES completion:nil];365}366367- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath368{369NSNumber *version = self.sortedJavaVersions[indexPath.section];370NSString *name = self.javaRuntimes[version][indexPath.row];371BOOL isInternal = [objc_getAssociatedObject(name, @"internalJRE") boolValue];372BOOL isInstalling = [objc_getAssociatedObject(name, @"installing") boolValue] && currentProgressView.fractionCompleted > 0.0f;373if (isInternal || isInstalling) {374return UITableViewCellEditingStyleNone;375} else {376return UITableViewCellEditingStyleDelete;377}378}379380#pragma mark Version parser381382- (NSString *)javaVersionForPath:(NSString *)path {383path = [path stringByAppendingPathComponent:@"release"];384NSError *error;385NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];386if (error) {387return [NSString stringWithFormat:@"Error: %@", error.localizedDescription];388}389390content = [content componentsSeparatedByString:@"JAVA_VERSION=\""][1];391content = [content componentsSeparatedByString:@"\""][0];392return content;393}394395- (NSNumber *)majorVersionForFullVersion:(NSString *)version {396if ([version hasPrefix:@"1.8.0"]) {397return @8;398} else if ([version hasPrefix:@"Error: "]){399return @(INVALID_JRE);400} else {401return @([version componentsSeparatedByString:@"."][0].intValue);402}403}404- (NSNumber *)majorVersionForPath:(NSString *)path {405return [self majorVersionForFullVersion:[self javaVersionForPath:path]];406}407408- (void)addRuntimePath:(NSString *)path markInternal:(BOOL)markInternal {409NSNumber *majorVer = [self majorVersionForPath:path];410if (!self.javaRuntimes[majorVer]) {411self.javaRuntimes[majorVer] = [NSMutableArray new];412[self.sortedJavaVersions addObject:majorVer];413[self.sortedJavaVersions sortUsingSelector:@selector(compare:)];414}415416NSString *file = path.lastPathComponent;417objc_setAssociatedObject(file, @"internalJRE", @(markInternal), OBJC_ASSOCIATION_RETAIN_NONATOMIC);418if (!markInternal) {419NSString *installingDir = [path stringByAppendingPathComponent:@".installing"];420BOOL markInstalling = [NSFileManager.defaultManager fileExistsAtPath:installingDir isDirectory:nil];421objc_setAssociatedObject(file, @"installing", @(markInstalling), OBJC_ASSOCIATION_RETAIN_NONATOMIC);422}423424[self.javaRuntimes[majorVer] addObject:file];425}426427- (void)removeRuntimeAtIndexPath:(NSIndexPath *)indexPath {428UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];429NSNumber *version = self.sortedJavaVersions[indexPath.section];430NSString *name = self.javaRuntimes[version][indexPath.row];431432[self.javaRuntimes[version] removeObject:name];433if (self.javaRuntimes[version].count == 0) {434[self.javaRuntimes removeObjectForKey:version];435[self.selectedRuntimes removeObjectForKey:version.stringValue];436[self.sortedJavaVersions removeObject:version];437} else if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {438self.selectedRuntimes[version.stringValue] = self.javaRuntimes[version][0];439}440441setPrefObject(@"java.java_homes", self.selectedRuntimes);442}443444- (void)listJREInPath:(NSString *)path markInternal:(BOOL)markInternal {445NSFileManager *fm = NSFileManager.defaultManager;446NSArray *files = [fm contentsOfDirectoryAtPath:path error:nil];447BOOL isDir;448for (NSString *file in files) {449NSString *rtPath = [path stringByAppendingPathComponent:file];450[fm fileExistsAtPath:rtPath isDirectory:&isDir];451if (isDir) {452[self addRuntimePath:rtPath markInternal:markInternal];453}454}455}456457#pragma mark Extract tar.xz458459+ (NSDictionary *)parseRuntimeInfo:(NSString *)path {460NSError *error;461462NSString *releaseFile = [path stringByAppendingPathComponent:@"release"];463NSString *content = [NSString stringWithContentsOfFile:releaseFile encoding:NSUTF8StringEncoding error:&error];464if (error) {465return @{@"Error": error.localizedDescription};466}467468NSMutableDictionary *dict = [NSMutableDictionary new];469for (NSString *line in [content componentsSeparatedByCharactersInSet:NSCharacterSet.newlineCharacterSet]) {470if (line.length > 0) {471NSArray *keyValue = [[line substringToIndex:line.length-1] componentsSeparatedByString:@"=\""];472dict[keyValue[0]] = keyValue[1];473}474}475return dict;476}477478+ (NSString *)validateRuntimeInfo:(NSString *)path {479NSDictionary *dict = [self parseRuntimeInfo:path];480481if (dict[@"Error"]) {482return dict[@"Error"];483} else if (![dict[@"OS_ARCH"] isEqualToString:@"aarch64"]) {484return [NSString stringWithFormat:@"Wrong runtime architecture: %@ (need aarch64)", dict[@"OS_ARCH"]];485} else if (![dict[@"OS_NAME"] isEqualToString:@"Darwin"]) {486return [NSString stringWithFormat:@"Wrong runtime platform: %@ (need Darwin)", dict[@"OS_NAME"]];487}488489dispatch_async(dispatch_get_main_queue(), ^{490[self.currentInstance addRuntimePath:path markInternal:NO];491[self.currentInstance.tableView reloadData];492});493494dispatch_async(dispatch_get_main_queue(), ^{495[self.currentInstance.tableView scrollToRowAtIndexPath:self.currentInstance.installingIndexPath496atScrollPosition:UITableViewScrollPositionBottom animated:YES];497});498return nil;499}500501+ (NSString *)lzmaErrorDescriptionForCode:(int)code {502switch (code) {503case LZMA_MEM_ERROR:504return @"Memory allocation failed";505case LZMA_FORMAT_ERROR:506return @"The input is not in the .xz format";507case LZMA_OPTIONS_ERROR:508return @"Unsupported compression options";509case LZMA_DATA_ERROR:510return @"Compressed file is corrupt";511case LZMA_BUF_ERROR:512return @"Compressed file is truncated or otherwise corrupt";513default:514return @"Unknown error, possibly a bug";515}516}517518// Reference: https://github.com/xz-mirror/xz/blob/master/doc/examples/02_decompress.c519+ (NSString *)extractTarXZ:(NSString *)inPath to:(NSString *)outPath progress:(NSProgress *)progress fileProgress:(NSProgress *)fileProgress fileCallback:(void(^)(NSString* name))fileCallback {520NSString *installingDir = [outPath stringByAppendingPathComponent:@".installing"];521[NSFileManager.defaultManager createDirectoryAtPath:installingDir withIntermediateDirectories:YES attributes:nil error:nil];522523NSInputStream *inFile = [NSInputStream inputStreamWithFileAtPath:inPath];524[inFile open];525526NSString *msg = nil;527lzma_stream strm = LZMA_STREAM_INIT;528lzma_action action = LZMA_RUN;529uint8_t inbuf[BUFSIZ], outbuf[512];530531lzma_ret ret = lzma_stream_decoder(&strm, UINT64_MAX, LZMA_CONCATENATED);532if (ret != LZMA_OK) {533return [self lzmaErrorDescriptionForCode:ret];534}535536strm.next_in = NULL;537strm.avail_in = 0;538strm.next_out = outbuf;539strm.avail_out = sizeof(outbuf);540541TarHeader currFileHeader;542NSString *currFileName;543NSOutputStream *currFileOut;544NSUInteger currFileOff, currFileSize;545546while (!progress.cancelled) {547if (strm.avail_in == 0 && inFile.hasBytesAvailable) {548strm.next_in = inbuf;549strm.avail_in = [inFile read:inbuf maxLength:sizeof(inbuf)];550551if (strm.avail_in == -1) {552msg = inFile.streamError.localizedDescription;553break;554}555556if (!inFile.hasBytesAvailable) {557action = LZMA_FINISH;558}559}560561ret = lzma_code(&strm, action);562if (strm.avail_out == 0 || ret == LZMA_STREAM_END) {563if (currFileOut) {564size_t remaining = currFileSize - currFileOff;565size_t write_size = MIN(sizeof(outbuf), remaining);566currFileOff += write_size;567// Avoid overloading the main queue568if (currFileOff % 102400 == 0) {569dispatch_async(dispatch_get_main_queue(), ^{570fileProgress.completedUnitCount = currFileOff;571fileCallback(currFileName);572});573}574if ([currFileOut write:outbuf maxLength:write_size] != write_size) {575msg = [NSString stringWithFormat:@"%s: %@", currFileHeader.name, currFileOut.streamError];576break;577} else if (currFileOff >= currFileSize) {578[currFileOut close];579currFileOut = nil;580if ([currFileName isEqualToString:@"./release"]) {581msg = [LauncherPrefManageJREViewController validateRuntimeInfo:outPath];582if (msg) goto cleanup;583}584}585} else {586memcpy(&currFileHeader, outbuf, sizeof(outbuf));587if (currFileHeader.name[0] == '\0') {588// EOF589break;590}591NSString *absPath = [NSString stringWithFormat:@"%@/%s", outPath, currFileHeader.name];592NSError *error = nil;593switch (currFileHeader.typeflag) {594case '0':595case '\0': { // File596currFileName = @(currFileHeader.name);597currFileOff = fileProgress.completedUnitCount = 0;598currFileSize = fileProgress.totalUnitCount = strtol(currFileHeader.size, NULL, 8);599NSLog(@"[RuntimeUnpack] Extracting %@", currFileName);600dispatch_async(dispatch_get_main_queue(), ^{601fileCallback(currFileName);602});603604currFileOut = [NSOutputStream outputStreamToFileAtPath:absPath append:NO];605[currFileOut open];606} break;607case '2': { // Symlink608symlink(currFileHeader.linkname, currFileHeader.name);609//NSLog(@"%s -> %s", currFileHeader.name, currFileHeader.linkname);610} break;611case '5': { // Folder612[NSFileManager.defaultManager createDirectoryAtPath:absPath withIntermediateDirectories:YES attributes:nil error:&error];613if (error) {614msg = [NSString stringWithFormat:@"%s: %@", currFileHeader.name, error];615goto cleanup;616}617} break;618default: // Ignore everything else619if (currFileHeader.typeflag < '0' && currFileHeader.typeflag > '7'620&& (currFileHeader.typeflag != 'x' && currFileHeader.typeflag != 'g')) {621msg = [NSString stringWithFormat:@"Invalid typeflag %c", currFileHeader.typeflag];622goto cleanup;623}624NSLog(@"[RuntimeUnpack] Skipped %s (typeflag %c)", currFileHeader.name, currFileHeader.typeflag);625break;626}627}628629strm.next_out = outbuf;630strm.avail_out = sizeof(outbuf);631632progress.completedUnitCount = strm.total_in;633}634635if (ret == LZMA_STREAM_END) {636break;637} else if (ret != LZMA_OK) {638msg = [self lzmaErrorDescriptionForCode:ret];639break;640}641}642643cleanup:644if (msg || progress.cancelled) {645[NSFileManager.defaultManager removeItemAtPath:outPath error:nil];646} else {647[NSFileManager.defaultManager removeItemAtPath:installingDir error:nil];648}649lzma_end(&strm);650[inFile close];651[currFileOut close];652return msg;653}654655@end656657658