From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Samuel Attard <samuel.r.attard@gmail.com>
Date: Mon, 28 Mar 2022 02:43:18 -0700
Subject: feat: add new Squirrel.Mac bundle installation method behind flag

The 'SquirrelMacEnableDirectContentsWrite' user default in your apps defaults suite
can be used to control this new installation method.  It is designed to remove the
requirement that the updating process have write access to its parent directory.

With this feature enabled the updating process only needs write access to it's own
.app bundle folder, not the owning /Applications folder.  This should allow more
non-admin users to update applications when appropriately granted group based permissions.

E.g. 775 and chown :staff

diff --git a/Squirrel/SQRLInstaller.m b/Squirrel/SQRLInstaller.m
index 7dd98ddee4ae0f4e01fd7aaa3486083bff7d0da1..c1f328fa8c3689218ef260347cb8f9d30b789efe 100644
--- a/Squirrel/SQRLInstaller.m
+++ b/Squirrel/SQRLInstaller.m
@@ -249,6 +249,7 @@ - (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request {
 		] reduce:^(NSURL *directoryURL, SQRLCodeSignature *codeSignature) {
 			NSURL *targetBundleURL = request.targetBundleURL;
 			NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
+			[NSFileManager.defaultManager createDirectoryAtURL:newBundleURL withIntermediateDirectories:FALSE attributes:nil error:nil];
 
 			return [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:request.targetBundleURL temporaryURL:newBundleURL codeSignature:codeSignature];
 		}]
@@ -481,10 +482,50 @@ - (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
 	NSParameterAssert(targetURL != nil);
 	NSParameterAssert(sourceURL != nil);
 
+	NSLog(@"Moving bundle from %@ to %@", sourceURL, targetURL);
+
+	// If both the sourceURL and the targetURL exist we can try to skip a permissions check
+	// by moving Thing.app/Contents directly.  This allows us to update applications without
+	// permission to write files into the parent directory of Thing.app
+	//
+	// There is no known case where these directories don't exist but in order to handle
+	// edge cases / race conditions we'll handle it anyway.
+	//
+	// This exists check is non-atomic with the rename call below but that's OK
+	BOOL canRenameContentsDirectly = FALSE;
+	// For now while this is tested at scale this new option is behind a user default, this
+	// can be set by applications wishing to test this feature at runtime.  If it causes issues
+	// it can be opted out by individual users by setting this key to false explicitly.
+	// Once this has bene tested at scale it will become the default for all Squirrel.Mac
+	// users.
+	NSUserDefaults *defaults = [[NSUserDefaults alloc] init];
+	[defaults addSuiteNamed:_applicationIdentifier];
+	// In cases where this code is being executed under the ShipIt executable it's running
+	// under an application identifier equal to {parent_identifier}.ShipIt
+	// In this case we need to use the true parent identifier too as that is 99% of the time
+	// where the key will be set.
+	if ([_applicationIdentifier hasSuffix:@".ShipIt"]) {
+		[defaults addSuiteNamed:[_applicationIdentifier substringToIndex:[_applicationIdentifier length] - 7]];
+	}
+
+	if ([defaults boolForKey:@"SquirrelMacEnableDirectContentsWrite"]) {
+		canRenameContentsDirectly = [NSFileManager.defaultManager fileExistsAtPath:targetURL.path] && [NSFileManager.defaultManager fileExistsAtPath:sourceURL.path];
+
+		if (canRenameContentsDirectly) {
+			NSLog(@"Moving bundles via 'Contents' folder rename");
+		} else {
+			NSLog(@"Moving bundles directly as one of source / target does not exist.  This is unexpected.");
+		}
+	} else {
+		NSLog(@"Moving bundles directly as SquirrelMacEnableDirectContentsWrite is disabled for app: %@", _applicationIdentifier);
+	}
+	NSURL *targetContentsURL = canRenameContentsDirectly ? [targetURL URLByAppendingPathComponent:@"Contents"] : targetURL;
+	NSURL *sourceContentsURL = canRenameContentsDirectly ? [sourceURL URLByAppendingPathComponent:@"Contents"] : sourceURL;
+
 	return [[[[RACSignal
 		defer:^{
 			// rename() is atomic, NSFileManager sucks.
-			if (rename(sourceURL.path.fileSystemRepresentation, targetURL.path.fileSystemRepresentation) == 0) {
+			if (rename(sourceContentsURL.path.fileSystemRepresentation, targetContentsURL.path.fileSystemRepresentation) == 0) {
 				return [RACSignal empty];
 			} else {
 				int code = errno;
@@ -497,24 +538,24 @@ - (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
 			}
 		}]
 		doCompleted:^{
-			NSLog(@"Moved bundle from %@ to %@", sourceURL, targetURL);
+			NSLog(@"Moved bundle contents from %@ to %@", sourceContentsURL, targetContentsURL);
 		}]
 		catch:^(NSError *error) {
 			if (![error.domain isEqual:NSPOSIXErrorDomain] || error.code != EXDEV) return [RACSignal error:error];
 
 			// If the locations lie on two different volumes, remove the
 			// destination by hand, then perform a move.
-			[NSFileManager.defaultManager removeItemAtURL:targetURL error:NULL];
+			[NSFileManager.defaultManager removeItemAtURL:targetContentsURL error:NULL];
 
-			if ([NSFileManager.defaultManager moveItemAtURL:sourceURL toURL:targetURL error:&error]) {
-				NSLog(@"Moved bundle across volumes from %@ to %@", sourceURL, targetURL);
+			if ([NSFileManager.defaultManager moveItemAtURL:sourceContentsURL toURL:targetContentsURL error:&error]) {
+				NSLog(@"Moved bundle contents across volumes from %@ to %@", sourceContentsURL, targetContentsURL);
 				return [RACSignal empty];
 			} else {
-				NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Couldn't move bundle %@ across volumes to %@", nil), sourceURL, targetURL];
+				NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Couldn't move bundle contents %@ across volumes to %@", nil), sourceContentsURL, targetContentsURL];
 				return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorMovingAcrossVolumes toError:error]];
 			}
 		}]
-		setNameWithFormat:@"%@ -installItemAtURL: %@ fromURL: %@", self, targetURL, sourceURL];
+		setNameWithFormat:@"%@ -installItemAtURL: %@ fromURL: %@", self, targetContentsURL, sourceContentsURL];
 }
 
 #pragma mark Quarantine Bit Removal
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
index c81c820d61da3c7d1cfd2c516147c954a5773a0c..4c703159a2bb0239b7d4e1793a985b5ec2edcfa9 100644
--- a/Squirrel/SQRLUpdater.m
+++ b/Squirrel/SQRLUpdater.m
@@ -329,7 +329,12 @@ - (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQ
 
 			BOOL targetWritable = [self canWriteToURL:targetURL];
 			BOOL parentWritable = [self canWriteToURL:targetURL.URLByDeletingLastPathComponent];
-			return [SQRLShipItLauncher launchPrivileged:!targetWritable || !parentWritable];
+			BOOL launchPrivileged = !targetWritable || !parentWritable;
+			if ([[NSUserDefaults standardUserDefaults] boolForKey:@"SquirrelMacEnableDirectContentsWrite"]) {
+				// If SquirrelMacEnableDirectContentsWrite is enabled we don't care if the parent directory is writeable or not
+				BOOL launchPrivileged = !targetWritable;
+			}
+			return [SQRLShipItLauncher launchPrivileged:launchPrivileged];
 		}]
 		replayLazily]
 		setNameWithFormat:@"shipItLauncher"];
