diff --git a/CMakeLists.txt b/CMakeLists.txt index e7f806e3bf..24f3192654 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,6 +125,7 @@ find_package(ZLIB REQUIRED) if(APPLE) find_package(Security) find_package(Metal) + find_package(LibArchive REQUIRED) elseif(UNIX) find_package(LIBSECRET) find_package(Uuid REQUIRED) diff --git a/Sources/Plasma/Apps/plClient/CMakeLists.txt b/Sources/Plasma/Apps/plClient/CMakeLists.txt index bd42ba35e5..8aba861fd3 100644 --- a/Sources/Plasma/Apps/plClient/CMakeLists.txt +++ b/Sources/Plasma/Apps/plClient/CMakeLists.txt @@ -246,6 +246,7 @@ target_link_libraries( CURL::libcurl "$<$:-framework Cocoa>" "$<$:-framework QuartzCore>" + $<$:${LibArchive_LIBRARIES}> ) target_include_directories(plClient PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSLoginWindowController.xib b/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSLoginWindowController.xib index ca19b0be2f..beb1a6ea1a 100644 --- a/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSLoginWindowController.xib +++ b/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSLoginWindowController.xib @@ -1,8 +1,8 @@ - + - - + + @@ -23,7 +23,7 @@ - + @@ -288,7 +288,7 @@ DQ - + @@ -335,6 +335,6 @@ Gw - + diff --git a/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.h b/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.h index 43fa529572..6e18ecffa2 100644 --- a/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.h +++ b/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.h @@ -50,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)patcher:(PLSPatcher*)patcher beganDownloadOfFile:(NSString*)file; - (void)patcher:(PLSPatcher*)patcher updatedProgress:(NSString*)progressMessage withBytes:(NSUInteger)bytes outOf:(uint64_t)totalBytes; -- (void)patcherCompleted:(PLSPatcher*)patcher; +- (void)patcherCompleted:(PLSPatcher*)patcher didSelfPatch:(BOOL)selfPatched; - (void)patcherCompletedWithError:(PLSPatcher*)patcher error:(NSError*)error; @end @@ -60,6 +60,7 @@ NS_ASSUME_NONNULL_BEGIN @property(weak) id delegate; @property(readonly) BOOL selfPatched; +- (NSURL*)completeSelfPatch:(NSError **)error; - (void)start; @end diff --git a/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.mm b/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.mm index 552df1776e..e6d3011b98 100644 --- a/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.mm +++ b/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.mm @@ -43,16 +43,20 @@ #import "PLSPatcher.h" #import "NSString+StringTheory.h" +#include +#include #include #include #include "HeadSpin.h" +#include "hsDarwin.h" #include "hsTimer.h" #include "pfPatcher/pfPatcher.h" #include "pfPatcher/plManifests.h" #include "plFileSystem.h" #include "plNetGameLib/plNetGameLib.h" +#include "plStatusLog/plStatusLog.h" class Patcher { @@ -61,6 +65,8 @@ void IOnPatchComplete(ENetError result, const ST::string& msg); void IOnProgressTick(uint64_t curBytes, uint64_t totalBytes, const ST::string& status); void IOnDownloadBegin(const plFileName& file); + void ISelfPatch(const plFileName& file); + plFileName IFindBundleExe(const plFileName& file); }; @interface PLSPatcher () @@ -68,6 +74,8 @@ @interface PLSPatcher () @property pfPatcher* patcher; @property NSTimer* networkPumpTimer; @property Patcher cppPatcher; +@property NSURL* updatedClientURL; +@property NSURL* temporaryDirectory; @end @implementation PLSPatcher @@ -88,6 +96,8 @@ - (id)init _patcher->OnCompletion(std::bind(&Patcher::IOnPatchComplete, _cppPatcher, std::placeholders::_1, std::placeholders::_2)); _patcher->OnFileDownloadDesired(IApproveDownload); + _patcher->OnSelfPatch(std::bind(&Patcher::ISelfPatch, _cppPatcher, std::placeholders::_1)); + _patcher->OnFindBundleExe(std::bind(&Patcher::IFindBundleExe, _cppPatcher, std::placeholders::_1)); self.networkPumpTimer = [NSTimer timerWithTimeInterval:1.0 / 1000.0 repeats:true @@ -106,6 +116,53 @@ - (void)start self.patcher->Start(); } +- (NSURL *)completeSelfPatch:(NSError **)error; +{ + NSString* destinationPath = [NSString stringWithSTString:plManifest::PatcherExecutable().AsString()]; + NSURL* destinationURL = [NSURL fileURLWithPath:destinationPath]; + + NSError* errorInScope; + + if (!self.updatedClientURL) { + // uh oh - this implies we weren't able to decompress the client + if (error) { + // Handle as a generic could not read file error. + // Bad compression on the server will require correction on the server end. + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoSuchFileError userInfo:nil]; + } + return nil; + } + + if ([NSFileManager.defaultManager fileExistsAtPath:destinationPath]) { + // need to swap + BOOL swapSucceeded = renamex_np(destinationURL.path.fileSystemRepresentation, self.updatedClientURL.path.fileSystemRepresentation, RENAME_SWAP) == 0; + if (swapSucceeded) { + // delete the old version - this is very likely us + // we want to terminate after. Our bundle will no longer be valid. + if (self.temporaryDirectory) { + [NSFileManager.defaultManager removeItemAtURL:self.temporaryDirectory error:&errorInScope]; + } + } else { + // abort and return an error + errorInScope = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]; + } + } else { + // no executable already present! Just move things into place. + [NSFileManager.defaultManager moveItemAtURL:self.updatedClientURL toURL:destinationURL error:&errorInScope]; + } + + if (errorInScope) { + // Try to clean up if there was an error + [NSFileManager.defaultManager removeItemAtURL:self.updatedClientURL error:nil]; + if (error) { + *error = errorInScope; + } + return nil; + } + + return destinationURL; +} + void Patcher::IOnDownloadBegin(const plFileName& file) { NSString* fileName = [NSString stringWithSTString:file.AsString()]; @@ -139,27 +196,190 @@ bool IApproveDownload(const plFileName& file) return extExcludeList.find(file.GetFileExt()) == extExcludeList.end(); } +static la_ssize_t copy_data(struct archive* ar, struct archive* aw) +{ + while (true) { + la_ssize_t r; + const void* buff; + size_t size; + la_int64_t offset; + + r = archive_read_data_block(ar, &buff, &size, &offset); + if (r == ARCHIVE_EOF) + return (ARCHIVE_OK); + if (r < ARCHIVE_OK) + return (r); + r = archive_write_data_block(aw, buff, size, offset); + if (r < ARCHIVE_OK) { + pfPatcher::GetLog()->AddLine(plStatusLog::kRed, archive_error_string(aw)); + return (r); + } + } +} + +void Patcher::ISelfPatch(const plFileName& file) +{ + /* + Note on errors: + This function does not return errors, but a self patch + without a populated updatedClientURL will imply something + went wrong during decompress. + */ + + PLSPatcher* patcher = parent; + patcher.selfPatched = true; + + int flags; + la_ssize_t r; + + /* Select which attributes we want to restore. */ + flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM; + + struct archive* a = archive_read_new(); + struct archive* ext = archive_write_disk_new(); + + { + int error; + error = archive_read_support_format_tar(a); + hsAssert(error == ARCHIVE_OK, "Unable to set tar format option"); + error = archive_read_support_filter_gzip(a); + hsAssert(error == ARCHIVE_OK, "Unable to set gzip filter"); + error = archive_read_support_filter_bzip2(a); + hsAssert(error == ARCHIVE_OK, "Unable to set bzip filter"); + + error = archive_write_disk_set_options(ext, flags); + hsAssert(error == ARCHIVE_OK, "Unable to set write options"); + error = archive_write_disk_set_standard_lookup(ext); + hsAssert(error == ARCHIVE_OK, "Unable to set write standard lookup"); + } + + if ((r = archive_read_open_filename(a, file.GetFileName().c_str(), 10240)) != ARCHIVE_OK) { + // couldn't read + archive_read_free(a); + archive_write_close(ext); + archive_write_free(ext); + return; + } + + NSError* error; + NSURL* currentDirectory = [NSURL fileURLWithPath:NSFileManager.defaultManager.currentDirectoryPath]; + patcher.temporaryDirectory = [NSFileManager.defaultManager + URLForDirectory:NSItemReplacementDirectory + inDomain:NSUserDomainMask + appropriateForURL:currentDirectory + create:YES error:&error]; + NSURL* outputURL; + if (patcher.temporaryDirectory) { + outputURL = [patcher.temporaryDirectory URLByAppendingPathComponent:[NSString stringWithSTString:plManifest::PatcherExecutable().GetFileName()]]; + [NSFileManager.defaultManager createDirectoryAtURL:outputURL withIntermediateDirectories:false attributes:nil error:&error]; + } + + if (error) { + // Not sure why things would go wrong, we should be able to + // get a writable temp directory. But if we could not, bail. + // Not populating the patched client path will be caught + // later. + archive_read_close(a); + archive_read_free(a); + archive_write_close(ext); + archive_write_free(ext); + return; + } + + ST::string outputPath = [outputURL.path STString]; + + bool succeeded = true; + + struct archive_entry* entry; + while (true) { + r = archive_read_next_header(a, &entry); + if (r == ARCHIVE_EOF) + break; + if (r < ARCHIVE_OK) + pfPatcher::GetLog()->AddLineF(plStatusLog::kRed, "Failed to read bundle archive: {}", archive_error_string(a)); + if (r < ARCHIVE_WARN) { + succeeded = false; + break; + } + const char* currentFile = archive_entry_pathname(entry); + auto fullOutputPath = plFileName::Join(outputPath, currentFile); + archive_entry_set_pathname(entry, fullOutputPath.AsString().c_str()); + r = archive_write_header(ext, entry); + if (r < ARCHIVE_OK) { + pfPatcher::GetLog()->AddLineF(plStatusLog::kRed, "Failed to extract file while patching app bundle: {}", archive_error_string(ext)); + } else if (archive_entry_size(entry) > 0) { + r = copy_data(a, ext); + if (r < ARCHIVE_OK) + pfPatcher::GetLog()->AddLineF(plStatusLog::kRed, "Failed to extract file while patching app bundle: {}", archive_error_string(ext)); + if (r < ARCHIVE_WARN) { + succeeded = false; + break; + } + } + r = archive_write_finish_entry(ext); + if (r < ARCHIVE_OK) + pfPatcher::GetLog()->AddLineF(plStatusLog::kRed, "Failed to extract file while patching app bundle: {}", archive_error_string(ext)); + if (r < ARCHIVE_WARN) { + succeeded = false; + break; + } + } + archive_read_close(a); + archive_read_free(a); + archive_write_close(ext); + archive_write_free(ext); + + plFileSystem::Unlink(file); + + if (succeeded) { + parent.updatedClientURL = outputURL; + } +} + void Patcher::IOnPatchComplete(ENetError result, const ST::string& msg) { [parent.networkPumpTimer invalidate]; if (IS_NET_SUCCESS(result)) { PLSPatcher* patcher = parent; dispatch_async(dispatch_get_main_queue(), ^{ - [patcher.delegate patcherCompleted:patcher]; + [patcher.delegate patcherCompleted:patcher + didSelfPatch:patcher.selfPatched]; }); } else { NSString* msgString = [NSString stringWithSTString:msg]; dispatch_async(dispatch_get_main_queue(), ^{ + ST::string errorString = ST::string::from_wchar(NetErrorToString(result)); + NSString* errorNSString = [NSString stringWithSTString:errorString]; [parent.delegate patcherCompletedWithError:parent error:[NSError errorWithDomain:@"PLSPatchErrors" code:result userInfo:@{ - NSLocalizedFailureErrorKey : msgString + NSLocalizedFailureErrorKey : errorNSString, + NSLocalizedFailureReasonErrorKey: msgString }]]; }); } } +plFileName Patcher::IFindBundleExe(const plFileName& clientPath) +{ + // If this is a Mac app bundle, MD5 the executable. The executable will hold the + // code signing hash - and thus unique the entire bundle. + + @autoreleasepool { + NSURL* bundleURL = [NSURL fileURLWithPath:[NSString stringWithSTString:clientPath.AsString()]]; + NSBundle* bundle = [NSBundle bundleWithURL:bundleURL]; + NSURL* executableURL = [bundle executableURL]; + + if (executableURL) { + NSString* executablePath = [executableURL path]; + return plFileName([[executableURL path] STString]); + } + + return clientPath; + } +} + @end diff --git a/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcherWindowController.mm b/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcherWindowController.mm index 6889834fd8..d6675e06ec 100644 --- a/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcherWindowController.mm +++ b/Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcherWindowController.mm @@ -81,12 +81,12 @@ - (void)patcher:(PLSPatcher*)patcher [NSString stringWithFormat:@"%@/%@", bytesString, totalBytesString]; } -- (void)patcherCompleted:(nonnull PLSPatcher*)patcher +- (void)patcherCompletedWithError:(nonnull PLSPatcher*)patcher error:(nonnull NSError*)error { // intercepted by the application } -- (void)patcherCompletedWithError:(nonnull PLSPatcher*)patcher error:(nonnull NSError*)error +- (void)patcherCompleted:(nonnull PLSPatcher *)patcher didSelfPatch:(BOOL)selfPatched { // intercepted by the application } diff --git a/Sources/Plasma/Apps/plClient/Mac-Cocoa/main.mm b/Sources/Plasma/Apps/plClient/Mac-Cocoa/main.mm index 0101728f20..2d4e3cb757 100644 --- a/Sources/Plasma/Apps/plClient/Mac-Cocoa/main.mm +++ b/Sources/Plasma/Apps/plClient/Mac-Cocoa/main.mm @@ -152,7 +152,8 @@ @interface AppDelegate : NSWindowController fFindBundleExe = std::move(vb); +} + void pfPatcher::OnCompletion(CompletionFunc cb) { fWorker->fOnComplete = std::move(cb); diff --git a/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.h b/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.h index 602091ea2a..348442b78e 100644 --- a/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.h +++ b/Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.h @@ -50,6 +50,8 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "pnNetBase/pnNbError.h" class plFileName; +class plFilePath; +class plMD5Checksum; class plStatusLog; class hsStream; @@ -84,9 +86,20 @@ class pfPatcher /** Represents a function that takes (bytesDLed, totalBytes, statsStr) as a progress indicator. */ typedef std::function ProgressTickFunc; + + /** Represents a function that takes (const plFileName&) and returns the executable inside the + * macOS application bundle at that path. + */ + typedef std::function FindBundleExeFunc; pfPatcher(); ~pfPatcher(); + + /** Set a callback that will be fired when the patcher needs to find an executable file + * within an executable bundle. This only occurs on the macOS client and is + * specific to macOS executable application bundles. + */ + void OnFindBundleExe(FindBundleExeFunc cb); /** Set a callback that will be fired when the patcher finishes its dirty work. * \remarks This may be called from any thread, so make sure your callback is diff --git a/Sources/Plasma/FeatureLib/pfPatcher/plManifests.cpp b/Sources/Plasma/FeatureLib/pfPatcher/plManifests.cpp index e6238497db..fde7be5e67 100644 --- a/Sources/Plasma/FeatureLib/pfPatcher/plManifests.cpp +++ b/Sources/Plasma/FeatureLib/pfPatcher/plManifests.cpp @@ -52,24 +52,44 @@ Mead, WA 99021 # define MANIFEST(in, ex) in #endif // PLASMA_EXTERNAL_RELEASE +#if defined(HS_BUILD_FOR_APPLE) +# define EXECUTABLE_SUFFIX ".app" +#elif defined(HS_BUILD_FOR_WIN32) +# define EXECUTABLE_SUFFIX ".exe" +#else +# define EXECUTABLE_SUFFIX "" +#endif + plFileName plManifest::ClientExecutable() { - return MANIFEST("plClient.exe", "UruExplorer.exe"); + return MANIFEST("plClient" EXECUTABLE_SUFFIX, "UruExplorer" EXECUTABLE_SUFFIX); } plFileName plManifest::PatcherExecutable() { - return MANIFEST("plUruLauncher.exe", "UruLauncher.exe"); +#ifdef HS_BUILD_FOR_MACOS + return MANIFEST("plClient" EXECUTABLE_SUFFIX, "UruExplorer" EXECUTABLE_SUFFIX); +#else + return MANIFEST("plUruLauncher" EXECUTABLE_SUFFIX, "UruLauncher" EXECUTABLE_SUFFIX); +#endif } ST::string plManifest::ClientManifest() { +#ifdef HS_BUILD_FOR_MACOS + return MANIFEST("macThinInternal", "macThinExternal"); +#else return MANIFEST("ThinInternal", "ThinExternal"); +#endif } ST::string plManifest::ClientImageManifest() { +#ifdef HS_BUILD_FOR_MACOS + return MANIFEST("macInternal", "macExternal"); +#else return MANIFEST("Internal", "External"); +#endif } ST::string plManifest::PatcherManifest() diff --git a/vcpkg.json b/vcpkg.json index 3b99d98bee..91d2b94133 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -11,6 +11,10 @@ "platform": "!(osx | windows)" }, "freetype", + { + "name": "libarchive", + "platform": "osx" + }, "libepoxy", "libjpeg-turbo", "libogg",