Skip to content

Instantly share code, notes, and snippets.

@core-code
Created November 16, 2021 08:50
Show Gist options
  • Save core-code/2002790687de4def4c6ebee7db75c195 to your computer and use it in GitHub Desktop.
Save core-code/2002790687de4def4c6ebee7db75c195 to your computer and use it in GitHub Desktop.
SUDiskImageUnarchiver.m without possible hang
//
// SUDiskImageUnarchiver.m
// Sparkle
//
// Created by Andy Matuschak on 6/16/08.
// Copyright 2008 Andy Matuschak. All rights reserved.
//
#import "SUDiskImageUnarchiver.h"
#import "SUUnarchiverNotifier.h"
#import "SUStandardVersionComparator.h"
#import "SUErrors.h"
@interface SUDiskImageUnarchiver ()
@property (nonatomic, copy, readonly) NSString *archivePath;
@property (nullable, nonatomic, copy, readonly) NSString *decryptionPassword;
@end
@implementation SUDiskImageUnarchiver
@synthesize archivePath = _archivePath;
@synthesize decryptionPassword = _decryptionPassword;
+ (BOOL)canUnarchivePath:(NSString *)path
{
return [[path pathExtension] isEqualToString:@"dmg"];
}
+ (BOOL)unsafeIfArchiveIsNotValidated
{
return NO;
}
- (instancetype)initWithArchivePath:(NSString *)archivePath decryptionPassword:(nullable NSString *)decryptionPassword
{
self = [super init];
if (self != nil) {
_archivePath = [archivePath copy];
_decryptionPassword = [decryptionPassword copy];
}
return self;
}
- (NSError *)unarchive
{
NSError *err = [self extractDMG];
return err;
}
#define SUSparkleErrorDomain @"SUSparkleErrorDomain"
// Called on a non-main thread.
- (NSError *)extractDMG
{
NSError *error;
@autoreleasepool
{
BOOL mountedSuccessfully = NO;
NSString *errorString = @"";
// get a unique mount point path
NSString *mountPoint = nil;
NSString *unmountPoint = nil;
NSString *resultStr;
NSString *errorStr;
NSFileManager *manager;
NSArray *contents;
do
{
// Using NSUUID would make creating UUIDs be done in Cocoa,
// and thus managed under ARC. Sadly, the class is in 10.8 and later.
CFUUIDRef uuid = CFUUIDCreate(NULL);
if (uuid)
{
NSString *uuidString = CFBridgingRelease(CFUUIDCreateString(NULL, uuid));
if (uuidString)
{
mountPoint = [@"/Volumes" stringByAppendingPathComponent:uuidString];
}
CFRelease(uuid);
}
}
// Note: this check does not follow symbolic links, which is what we want
while ([[NSURL fileURLWithPath:mountPoint] checkResourceIsReachableAndReturnError:NULL]);
NSData *promptData = nil;
promptData = [NSData dataWithBytes:"yes\n" length:4];
NSMutableArray *arguments = [@[@"attach", self.archivePath, @"-mountpoint", mountPoint, /*@"-noverify",*/ @"-nobrowse", @"-noautoopen", @"-verbose"] mutableCopy];
NSString *destination = [self.archivePath stringByDeletingLastPathComponent];
destination = [destination stringByAppendingPathComponent:makeString(@"%@_folder", self.archivePath.lastPathComponent.stringByDeletingPathExtension)];
[NSFileManager.defaultManager createDirectoryAtURL:destination.fileURL withIntermediateDirectories:YES attributes:nil error:NULL];
NSData *output = nil, *outputError = nil;
NSInteger taskResult = -1;
@try
{
NSTask *task = [[NSTask alloc] init];
task.launchPath = @"/usr/bin/hdiutil";
task.currentDirectoryPath = @"/";
task.arguments = arguments;
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSMutableString *jobStdOutput = makeMutableString();
NSMutableString *jobErrOutput = makeMutableString();
NSPipe *taskStdPipe = [NSPipe pipe];
NSPipe *taskErrPipe = [NSPipe pipe];
NSPipe *inputPipe = [NSPipe pipe];
task.standardOutput = taskStdPipe;
task.standardError = taskErrPipe;
task.standardInput = inputPipe;
NSFileHandle *fileStdHandle = taskStdPipe.fileHandleForReading;
NSFileHandle *fileErrHandle = taskErrPipe.fileHandleForReading;
[fileStdHandle setReadabilityHandler:^(NSFileHandle *file)
{
NSData *data = file.availableData;
NSString *string = data.string;
if (string)
[jobStdOutput appendString:string];
}];
[fileErrHandle setReadabilityHandler:^(NSFileHandle *file)
{
NSData *data = file.availableData;
NSString *string = data.string;
if (string)
[jobErrOutput appendString:string];
}];
[task setTerminationHandler:^(NSTask *t)
{
fileStdHandle.readabilityHandler = nil;
fileErrHandle.readabilityHandler = nil;
dispatch_semaphore_signal(sema);
}];
[task launch];
[inputPipe.fileHandleForWriting writeData:promptData];
[inputPipe.fileHandleForWriting closeFile];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
sema = NULL;
output = jobStdOutput.data;
outputError = jobErrOutput.data;
taskResult = task.terminationStatus;
}
@catch (NSException *e)
{
errorString = e.description;
goto reportError;
}
if (output) resultStr = [[NSString alloc] initWithData:output encoding:NSUTF8StringEncoding];
if (outputError) errorString = [[NSString alloc] initWithData:outputError encoding:NSUTF8StringEncoding];
if (taskResult != 0)
{
errorString = makeString(@"hdiutil failed with code: %ld data: <<%@>> error: <<%@>>", (long)taskResult, resultStr, errorStr);
NSLog(@"%@", errorString);
goto reportError;
}
unmountPoint = mountPoint;
if ([resultStr contains:@"Apple_APFS"])
{
for (NSString *line in [resultStr componentsSeparatedByString:@"\n"])
{
if ([line rangeOfString:@"GUID_partition_scheme"].location != NSNotFound && [line rangeOfString:@"/dev/disk"].location != NSNotFound)
unmountPoint = [[line stringByReplacingOccurrencesOfString:@"GUID_partition_scheme" withString:@""] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
}
mountedSuccessfully = YES;
// Now that we've mounted it, we need to copy out its contents.
manager = [[NSFileManager alloc] init];
contents = [manager contentsOfDirectoryAtPath:mountPoint error:&error];
if (error)
{
errorString = makeString(@"Couldn't enumerate contents of archive mounted at %@: %@", mountPoint, error);
NSLog(@"%@", errorString);
goto reportError;
}
double itemsCopied = 0;
for (NSString *item in contents)
{
NSString *fromPath = [mountPoint stringByAppendingPathComponent:item];
NSString *toPath = [destination stringByAppendingPathComponent:item];
// We skip any files in the DMG which are not readable.
if (![manager isReadableFileAtPath:fromPath]) {
continue;
}
if ([item isEqualToString:@".Spotlight-V100"]) {
continue;
}
itemsCopied += 1.0;
NSLog(@"copyItemAtPath:%@ toPath:%@", fromPath, toPath);
if (![manager copyItemAtPath:fromPath toPath:toPath error:&error])
{
errorString = makeString(@"Couldn't copyItemAtPath:%@ toPath:%@ error:%@", fromPath, toPath, error.description);
NSLog(@"%@", errorString);
goto reportError;
}
}
error = nil;
goto finally;
reportError:
if (!error)
error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:
@{@"info" : @"extractDMG failed",
@"error" : ([errorString contains:@"expected CRC32"] || [errorString contains:@"error 1000 calculating checksum"] ) ? @"err1crc" : errorString}];
finally:
if (mountedSuccessfully) {
NSTask *task = [[NSTask alloc] init];
task.launchPath = @"/usr/bin/hdiutil";
task.arguments = @[@"detach", unmountPoint, @"-force"];
@try {
[task launch];
} @catch (NSException *exception) {
NSLog(@"Failed to unmount %@ Exception: %@", unmountPoint, exception);
}
task = nil;
} else {
NSLog(@"Can't mount DMG %@", self.archivePath);
}
}
return error;
}
- (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], self.archivePath]; }
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment