Skip to content

Commit

Permalink
Skip extracting auxiliary files and improve extraction progress for d…
Browse files Browse the repository at this point in the history
…isk images (#2569)
  • Loading branch information
zorgiepoo authored May 27, 2024
1 parent 444a537 commit 27cf3b3
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 36 deletions.
138 changes: 109 additions & 29 deletions Autoupdate/SUDiskImageUnarchiver.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@

#include "AppKitPrevention.h"

@interface SUDiskImageUnarchiver () <NSFileManagerDelegate>
@end

@implementation SUDiskImageUnarchiver
{
NSString *_archivePath;
NSString *_decryptionPassword;
NSString *_extractionDirectory;

SUUnarchiverNotifier *_notifier;
double _currentExtractionProgress;
double _fileProgressIncrement;
}

+ (BOOL)canUnarchivePath:(NSString *)path
Expand Down Expand Up @@ -51,6 +58,25 @@ - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBl
});
}

static NSUInteger fileCountForDirectory(NSFileManager *fileManager, NSString *itemPath)
{
NSUInteger fileCount = 0;
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:itemPath];
for (NSString * __unused currentFile in dirEnum) {
fileCount++;
}

return fileCount;
}

- (BOOL)fileManager:(NSFileManager *)fileManager shouldCopyItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL
{
_currentExtractionProgress += _fileProgressIncrement;
[_notifier notifyProgress:_currentExtractionProgress];

return YES;
}

// Called on a non-main thread.
- (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
{
Expand All @@ -62,13 +88,12 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
NSFileManager *manager;
NSError *error = nil;
NSArray *contents = nil;
do
{
do {
NSString *uuidString = [[NSUUID UUID] UUIDString];
mountPoint = [@"/Volumes" stringByAppendingPathComponent:uuidString];
}
}
// Note: this check does not follow symbolic links, which is what we want
while ([[NSURL fileURLWithPath:mountPoint] checkResourceIsReachableAndReturnError:NULL]);
while ([[NSURL fileURLWithPath:mountPoint] checkResourceIsReachableAndReturnError:NULL]);

NSData *promptData = [NSData dataWithBytes:"yes\n" length:4];

Expand All @@ -92,6 +117,11 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
NSData *output = nil;
NSInteger taskResult = -1;

NSURL *mountPointURL = [NSURL fileURLWithPath:mountPoint isDirectory:YES];
NSURL *extractionDirectoryURL = [NSURL fileURLWithPath:_extractionDirectory isDirectory:YES];
NSMutableArray<NSString *> *itemsToExtract = [NSMutableArray array];
NSUInteger totalFileExtractionCount = 0;

{
NSTask *task = [[NSTask alloc] init];
task.launchPath = @"/usr/bin/hdiutil";
Expand Down Expand Up @@ -122,8 +152,6 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
goto reportError;
}

[notifier notifyProgress:0.125];

if (@available(macOS 10.15, *)) {
if (![inputPipe.fileHandleForWriting writeData:promptData error:&error]) {
goto reportError;
Expand All @@ -147,50 +175,102 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT

taskResult = task.terminationStatus;
}

[notifier notifyProgress:0.5];

if (taskResult != 0)
{
if (taskResult != 0) {
NSString *resultStr = output ? [[NSString alloc] initWithData:output encoding:NSUTF8StringEncoding] : nil;
SULog(SULogLevelError, @"hdiutil failed with code: %ld data: <<%@>>", (long)taskResult, resultStr);
goto reportError;
}
mountedSuccessfully = YES;

// Mounting can take some time, so increment progress
_currentExtractionProgress = 0.1;
[notifier notifyProgress:_currentExtractionProgress];

// Now that we've mounted it, we need to copy out its contents.
manager = [[NSFileManager alloc] init];
contents = [manager contentsOfDirectoryAtPath:mountPoint error:&error];
if (contents == nil)
{
if (contents == nil) {
SULog(SULogLevelError, @"Couldn't enumerate contents of archive mounted at %@: %@", mountPoint, error);
goto reportError;
}

double itemsCopied = 0;
double totalItems = (double)[contents count];

for (NSString *item in contents)
{
NSString *fromPath = [mountPoint stringByAppendingPathComponent:item];
NSString *toPath = [_extractionDirectory stringByAppendingPathComponent:item];

// Sparkle can support installing pkg files, app bundles, and other bundle types for plug-ins
// We must not filter any of those out
for (NSString *item in contents) {
NSURL *fromPathURL = [mountPointURL URLByAppendingPathComponent:item];

itemsCopied += 1.0;
[notifier notifyProgress:0.5 + itemsCopied/(totalItems*2.0)];
NSString *lastPathComponent = fromPathURL.lastPathComponent;

// We skip any files in the DMG which are not readable but include the item in the progress
if (![manager isReadableFileAtPath:fromPath]) {
// Ignore hidden files
if ([lastPathComponent hasPrefix:@"."]) {
continue;
}

SULog(SULogLevelDefault, @"copyItemAtPath:%@ toPath:%@", fromPath, toPath);

if (![manager copyItemAtPath:fromPath toPath:toPath error:&error])
{

// Ignore aliases
NSNumber *aliasFlag = nil;
if ([fromPathURL getResourceValue:&aliasFlag forKey:NSURLIsAliasFileKey error:NULL] && aliasFlag.boolValue) {
continue;
}

// Ignore symbolic links
NSNumber *symbolicFlag = nil;
if ([fromPathURL getResourceValue:&symbolicFlag forKey:NSURLIsSymbolicLinkKey error:NULL] && symbolicFlag.boolValue) {
continue;
}

// Ensure file is readable
NSNumber *isReadableFlag = nil;
if ([fromPathURL getResourceValue:&isReadableFlag forKey:NSURLIsReadableKey error:NULL] && !isReadableFlag.boolValue) {
continue;
}

NSNumber *isDirectoryFlag = nil;
if (![fromPathURL getResourceValue:&isDirectoryFlag forKey:NSURLIsDirectoryKey error:NULL]) {
continue;
}

BOOL isDirectory = isDirectoryFlag.boolValue;
NSString *pathExtension = fromPathURL.pathExtension;

if (isDirectory) {
// Skip directory types that aren't bundles or regular directories
if ([pathExtension isEqualToString:@"rtfd"]) {
continue;
}
} else {
// The only non-directory files we care about are (m)pkg files
if (![pathExtension isEqualToString:@"pkg"] && ![pathExtension isEqualToString:@"mpkg"]) {
continue;
}
}

if (isDirectory) {
totalFileExtractionCount += fileCountForDirectory(manager, fromPathURL.path);
} else {
totalFileExtractionCount++;
}

[itemsToExtract addObject:item];
}

_fileProgressIncrement = (0.99 - _currentExtractionProgress) / totalFileExtractionCount;
_notifier = notifier;

// Copy all items we want to extract and notify of progress
manager.delegate = self;
for (NSString *item in itemsToExtract) {
NSURL *fromURL = [mountPointURL URLByAppendingPathComponent:item];
NSURL *toURL = [extractionDirectoryURL URLByAppendingPathComponent:item];

if (![manager copyItemAtURL:fromURL toURL:toURL error:&error]) {
SULog(SULogLevelError, @"Failed to copy '%@' to '%@' with error: %@", fromURL.path, toURL.path, error);
goto reportError;
}
}

[notifier notifyProgress:1.0];

[notifier notifySuccess];
goto finally;

Expand Down
8 changes: 8 additions & 0 deletions Sparkle.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
724BB3AA1D3347C2005D534A /* SUInstallerStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 724BB3971D333832005D534A /* SUInstallerStatus.m */; };
724BB3B71D35ABA8005D534A /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B5F8F609C4CEB300B25A18 /* Security.framework */; };
724F76F91D6EAD0D00ECD062 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 525A278F133D6AE900FD8D70 /* Cocoa.framework */; };
725453552C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg */; };
725602D51C83551C00DAA70E /* SUApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 725602D31C83551C00DAA70E /* SUApplicationInfo.h */; };
725602D61C83551C00DAA70E /* SUApplicationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 725602D41C83551C00DAA70E /* SUApplicationInfo.m */; };
725B3A82263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml in Resources */ = {isa = PBXBuildFile; fileRef = 725B3A81263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml */; };
Expand Down Expand Up @@ -411,6 +412,7 @@
72E45CF81B640DAE005C701A /* SUUpdateSettingsWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 72E45CF61B640DAE005C701A /* SUUpdateSettingsWindowController.xib */; };
72E45CFC1B641961005C701A /* sparkletestcast.xml in Resources */ = {isa = PBXBuildFile; fileRef = 72E45CFB1B641961005C701A /* sparkletestcast.xml */; };
72E539121D68C3FA0092CE5E /* SPUDownloadData.m in Sources */ = {isa = PBXBuildFile; fileRef = 72F9EC431D5E9ED8004AC8B6 /* SPUDownloadData.m */; };
72E6D9712C04DE1A005496E4 /* SparkleTestCodeSign_pkg.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 72E6D9702C04DE19005496E4 /* SparkleTestCodeSign_pkg.dmg */; };
72EB735F29BE981300FBCEE7 /* DevSignedApp.zip in Resources */ = {isa = PBXBuildFile; fileRef = 72EB735E29BE981300FBCEE7 /* DevSignedApp.zip */; };
72EB736129BEB36100FBCEE7 /* DevSignedAppVersion2.zip in Resources */ = {isa = PBXBuildFile; fileRef = 72EB736029BEB36100FBCEE7 /* DevSignedAppVersion2.zip */; };
72EB87EA1CB8798800C37F42 /* ShowInstallerProgress.m in Sources */ = {isa = PBXBuildFile; fileRef = 72EB87E91CB8798800C37F42 /* ShowInstallerProgress.m */; };
Expand Down Expand Up @@ -1226,6 +1228,7 @@
724BB3A61D33461B005D534A /* SUXPCInstallerStatus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUXPCInstallerStatus.h; sourceTree = "<group>"; };
724BB3A71D33461B005D534A /* SUXPCInstallerStatus.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUXPCInstallerStatus.m; sourceTree = "<group>"; };
724BB3B51D35AAC3005D534A /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; };
725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSign_apfs_lzma_aux_files.dmg; sourceTree = "<group>"; };
725602D31C83551C00DAA70E /* SUApplicationInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUApplicationInfo.h; sourceTree = "<group>"; };
725602D41C83551C00DAA70E /* SUApplicationInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUApplicationInfo.m; sourceTree = "<group>"; };
72563CA9272E1C5400AF39F0 /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1421,6 +1424,7 @@
72E45CF51B640DAE005C701A /* SUUpdateSettingsWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUUpdateSettingsWindowController.m; sourceTree = "<group>"; };
72E45CF61B640DAE005C701A /* SUUpdateSettingsWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SUUpdateSettingsWindowController.xib; sourceTree = "<group>"; };
72E45CFB1B641961005C701A /* sparkletestcast.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = sparkletestcast.xml; sourceTree = "<group>"; };
72E6D9702C04DE19005496E4 /* SparkleTestCodeSign_pkg.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSign_pkg.dmg; sourceTree = "<group>"; };
72EB735E29BE981300FBCEE7 /* DevSignedApp.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = DevSignedApp.zip; sourceTree = "<group>"; };
72EB736029BEB36100FBCEE7 /* DevSignedAppVersion2.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = DevSignedAppVersion2.zip; sourceTree = "<group>"; };
72EB87E91CB8798800C37F42 /* ShowInstallerProgress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ShowInstallerProgress.m; path = Sparkle/InstallerProgress/ShowInstallerProgress.m; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -1855,6 +1859,8 @@
C23E88591BE7AF890050BB73 /* SparkleTestCodeSignApp.enc.dmg */,
72AC6B271B9AAD6700F62325 /* SparkleTestCodeSignApp.tar */,
72BC6C3C275027BF0083F14B /* SparkleTestCodeSign_apfs.dmg */,
725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg */,
72E6D9702C04DE19005496E4 /* SparkleTestCodeSign_pkg.dmg */,
72AC6B291B9AAF3A00F62325 /* SparkleTestCodeSignApp.tar.bz2 */,
72AC6B251B9AAC8800F62325 /* SparkleTestCodeSignApp.tar.gz */,
72AC6B2B1B9AB0EE00F62325 /* SparkleTestCodeSignApp.tar.xz */,
Expand Down Expand Up @@ -3167,13 +3173,15 @@
72EB736129BEB36100FBCEE7 /* DevSignedAppVersion2.zip in Resources */,
723EDC3F26885A8E000BCBA4 /* testappcast_channels.xml in Resources */,
720DC50427A51A6500DFF3EC /* testappcast_minimumAutoupdateVersionSkipping.xml in Resources */,
72E6D9712C04DE1A005496E4 /* SparkleTestCodeSign_pkg.dmg in Resources */,
72AC6B2E1B9B218C00F62325 /* SparkleTestCodeSignApp.dmg in Resources */,
C23E885B1BE7B24F0050BB73 /* SparkleTestCodeSignApp.enc.dmg in Resources */,
72AC6B281B9AAD6700F62325 /* SparkleTestCodeSignApp.tar in Resources */,
72AC6B2A1B9AAF3A00F62325 /* SparkleTestCodeSignApp.tar.bz2 in Resources */,
72AC6B261B9AAC8800F62325 /* SparkleTestCodeSignApp.tar.gz in Resources */,
72AC6B2C1B9AB0EE00F62325 /* SparkleTestCodeSignApp.tar.xz in Resources */,
729F7ECE27409077004592DC /* SparkleTestCodeSignApp_bad_extraneous.zip in Resources */,
725453552C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg in Resources */,
5A5DD41D249F116E0045EB3E /* test-relative-urls.xml in Resources */,
F8761EB31ADC50EB000C9034 /* SparkleTestCodeSignApp.zip in Resources */,
5A5DD40424958B000045EB3E /* SUUpdateValidatorTest in Resources */,
Expand Down
Binary file not shown.
Binary file added Tests/Resources/SparkleTestCodeSign_pkg.dmg
Binary file not shown.
25 changes: 20 additions & 5 deletions Tests/SUUnarchiverTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,21 @@ class SUUnarchiverTest: XCTestCase
let unarchivedSuccessExpectation = super.expectation(description: "Unarchived Success (format: \(archiveExtension))")
let unarchivedFailureExpectation = super.expectation(description: "Unarchived Failure (format: \(archiveExtension))")

let extractedAppURL = tempDirectoryURL.appendingPathComponent(extractedAppName).appendingPathExtension("app")

self.unarchiveTestAppWithExtension(archiveExtension, appName: appName, tempDirectoryURL: tempDirectoryURL, archiveResourceURL: archiveResourceURL, password: password, expectingInstallationType: installationType, expectingSuccess: expectingSuccess, testExpectation: unarchivedSuccessExpectation)
self.unarchiveNonExistentFileTestFailureAppWithExtension(archiveExtension, tempDirectoryURL: tempDirectoryURL, password: password, expectingInstallationType: installationType, testExpectation: unarchivedFailureExpectation)

super.waitForExpectations(timeout: 30.0, handler: nil)

if !archiveExtension.hasSuffix("pkg") && expectingSuccess {
XCTAssertTrue(fileManager.fileExists(atPath: extractedAppURL.path))
XCTAssertEqual("6a60ab31430cfca8fb499a884f4a29f73e59b472", hashOfTree(extractedAppURL.path))
if expectingSuccess {
if installationType == SPUInstallationTypeApplication {
let extractedAppURL = tempDirectoryURL.appendingPathComponent(extractedAppName).appendingPathExtension("app")

XCTAssertTrue(fileManager.fileExists(atPath: extractedAppURL.path))
XCTAssertEqual("6a60ab31430cfca8fb499a884f4a29f73e59b472", hashOfTree(extractedAppURL.path))
} else if archiveExtension != "pkg" {
let extractedPackageURL = tempDirectoryURL.appendingPathComponent(extractedAppName).appendingPathExtension("pkg")
XCTAssertTrue(fileManager.fileExists(atPath: extractedPackageURL.path))
}
}
}

Expand Down Expand Up @@ -125,6 +130,16 @@ class SUUnarchiverTest: XCTestCase
{
self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_apfs")
}

func testUnarchivingAPFSAdhocSignedDMGWithAuxFiles()
{
self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_apfs_lzma_aux_files")
}

func testUnarchivingAPFSDMGWithPackage()
{
self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_pkg", expectingInstallationType: SPUInstallationTypeGuidedPackage)
}
#endif

#if SPARKLE_BUILD_PACKAGE_SUPPORT
Expand Down
21 changes: 19 additions & 2 deletions sparkle-cli/SPUCommandLineUserDriver.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
@implementation SPUCommandLineUserDriver
{
SUUpdatePermissionResponse *_updatePermissionResponse;
NSString *_lastProgressReported;

uint64_t _bytesDownloaded;
uint64_t _bytesToDownload;
Expand Down Expand Up @@ -166,6 +167,7 @@ - (void)showUpdaterError:(NSError *)__unused error acknowledgement:(void (^)(voi
- (void)showDownloadInitiatedWithCancellation:(void (^)(void))__unused cancellation
{
if (_verbose) {
_lastProgressReported = nil;
fprintf(stderr, "Downloading Update...\n");
}
}
Expand All @@ -189,21 +191,36 @@ - (void)showDownloadDidReceiveDataOfLength:(uint64_t)length
}

if (_bytesToDownload > 0 && _verbose) {
fprintf(stderr, "Downloaded %llu out of %llu bytes (%.0f%%)\n", _bytesDownloaded, _bytesToDownload, (_bytesDownloaded * 100.0 / _bytesToDownload));
NSString *currentProgressPercentage = [NSString stringWithFormat:@"%.0f%%", (_bytesDownloaded * 100.0 / _bytesToDownload)];

// Only report progress advancement when percentage significantly advances
if (_lastProgressReported == nil || ![_lastProgressReported isEqualToString:currentProgressPercentage]) {
fprintf(stderr, "Downloaded %llu out of %llu bytes (%s)\n", _bytesDownloaded, _bytesToDownload, currentProgressPercentage.UTF8String);

_lastProgressReported = currentProgressPercentage;
}
}
}

- (void)showDownloadDidStartExtractingUpdate
{
if (_verbose) {
_lastProgressReported = nil;
fprintf(stderr, "Extracting Update...\n");
}
}

- (void)showExtractionReceivedProgress:(double)progress
{
if (_verbose) {
fprintf(stderr, "Extracting Update (%.0f%%)\n", progress * 100);
NSString *currentProgressPercentage = [NSString stringWithFormat:@"%.0f%%", progress * 100];

// Only report progress advancement when percentage significantly advances
if (_lastProgressReported == nil || ![_lastProgressReported isEqualToString:currentProgressPercentage]) {
fprintf(stderr, "Extracting Update (%s)\n", currentProgressPercentage.UTF8String);

_lastProgressReported = currentProgressPercentage;
}
}
}

Expand Down

0 comments on commit 27cf3b3

Please sign in to comment.