From 93c37a673f3fb989e90102fb3d65ed5dab1c50b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliv=C3=A9r=20Falvai?= Date: Tue, 25 Aug 2026 17:49:32 +0200 Subject: [PATCH] iOS: apply bsdiff patches during package install --- docs/setup-ios.md | 16 +++++ ios/CodePush/CodePush.h | 1 + ios/CodePush/CodePushConfig.m | 3 +- ios/CodePush/CodePushPackage.m | 108 +++++++++++++++++++++++++++++++-- 4 files changed, 122 insertions(+), 6 deletions(-) diff --git a/docs/setup-ios.md b/docs/setup-ios.md index 3918f9c1..b9f84dbb 100644 --- a/docs/setup-ios.md +++ b/docs/setup-ios.md @@ -136,3 +136,19 @@ MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANkWYydPuyOumR/sn2agNBVDnzyRpM16NAUpYPGxNgjSEp0e ``` +### Enable Delta Updates + +Switch for applying binary diff (bsdiff) patches during a diff update, off by default (at the moment). When disabled, only file-by-file diffing is applied (for example, skipping assets if only the main JS bundle changed, but that whole file is downloaded byte for byte). Add a `CodePushEnableDeltaUpdates` boolean record to `Info.plist` to turn it on: + +```xml + + + + + CodePushEnableDeltaUpdates + + + + + +``` diff --git a/ios/CodePush/CodePush.h b/ios/CodePush/CodePush.h index a138bd40..644bdeeb 100644 --- a/ios/CodePush/CodePush.h +++ b/ios/CodePush/CodePush.h @@ -103,6 +103,7 @@ @property (copy) NSString *deploymentKey; @property (copy) NSString *serverURL; @property (copy) NSString *publicKey; +@property (readonly) BOOL enableDeltaUpdates; + (instancetype)current; diff --git a/ios/CodePush/CodePushConfig.m b/ios/CodePush/CodePushConfig.m index 029d4180..3a6e3091 100644 --- a/ios/CodePush/CodePushConfig.m +++ b/ios/CodePush/CodePushConfig.m @@ -36,7 +36,8 @@ - (instancetype)init NSString *deploymentKey = [infoDictionary objectForKey:@"CodePushDeploymentKey"]; NSString *serverURL = [infoDictionary objectForKey:@"CodePushServerURL"]; NSString *publicKey = [infoDictionary objectForKey:@"CodePushPublicKey"]; - + _enableDeltaUpdates = [[infoDictionary objectForKey:@"CodePushEnableDeltaUpdates"] boolValue]; + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; NSString *clientUniqueId = [userDefaults stringForKey:ClientUniqueIDConfigKey]; if (clientUniqueId == nil) { diff --git a/ios/CodePush/CodePushPackage.m b/ios/CodePush/CodePushPackage.m index eb5b331b..43088bec 100644 --- a/ios/CodePush/CodePushPackage.m +++ b/ios/CodePush/CodePushPackage.m @@ -1,6 +1,7 @@ #import "CodePush.h" #import "CodePushDiffManifest.h" #import "CodePushErrorUtils.h" +#import "CodePushBinaryDiffPatcher.h" #if __has_include() #import #else @@ -12,6 +13,8 @@ @implementation CodePushPackage #pragma mark - Private constants static NSString *const DiffManifestFileName = @"hotcodepush.json"; +// Folder within the update ZIP that contains the diff patches. +static NSString *const DiffPatchesFolderName = @"__hcp_patches"; static NSString *const DownloadFileName = @"download.zip"; static NSString *const RelativeBundlePathKey = @"bundlePath"; static NSString *const StatusFile = @"codepush.json"; @@ -19,6 +22,78 @@ @implementation CodePushPackage static NSString *const UpdateMetadataFileName = @"app.json"; static NSString *const UnzippedFolderName = @"unzipped"; +#pragma mark - Private methods + ++ (BOOL)validateDiffManifest:(CodePushDiffManifest *)diffManifest + currentPackageFolder:(NSString *)currentPackageFolderPath + enableDeltaUpdates:(BOOL)enableDeltaUpdates + error:(NSError **)error +{ + if (diffManifest.version > 2 || diffManifest.version < 1) { + if (error) { + *error = [CodePushErrorUtils errorWithMessage: + [NSString stringWithFormat:@"Diff manifest version %ld is not supported by this SDK version.", (long)diffManifest.version]]; + } + return NO; + } else if (diffManifest.version == 2 && !enableDeltaUpdates) { + if (error) { + *error = [CodePushErrorUtils errorWithMessage: + @"Received a binary diff update, but delta updates are not enabled on this client. Set CodePushEnableDeltaUpdates to true in Info.plist to enable them."]; + } + return NO; + } else if (diffManifest.version == 2 && currentPackageFolderPath == nil) { + if (error) { + *error = [CodePushErrorUtils errorWithMessage: + @"Received a binary diff update, but no currently installed package exists to diff against (this is likely the first CodePush update for this app install). Diffing against the embedded app binary is not yet supported."]; + } + return NO; + } + + return YES; +} + ++ (BOOL)applyDiffManifest:(CodePushDiffManifest *)diffManifest + currentPackageFolder:(NSString *)currentPackageFolderPath + unzippedFolder:(NSString *)unzippedFolderPath + newUpdateFolder:(NSString *)newUpdateFolderPath + error:(NSError **)error +{ + if (diffManifest.version != 2) { + return YES; + } + + NSError *patchError = nil; + BOOL patchesApplied = [CodePushBinaryDiffPatcher applyBinaryDiffPatchesFromManifest:diffManifest + currentPackageFolder:currentPackageFolderPath + unzippedFolder:unzippedFolderPath + newUpdateFolder:newUpdateFolderPath + error:&patchError]; + if (!patchesApplied) { + if (error) { + *error = patchError ?: [CodePushErrorUtils errorWithMessage:@"Failed to apply the binary diff patches of this update."]; + } + return NO; + } + + // The patches folder must not stay in the installed package: it is + // not part of the released contents, so it changes the folder hash + // and surfaces later as a misleading integrity-check failure. + NSString *patchesFolderPath = [newUpdateFolderPath stringByAppendingPathComponent:DiffPatchesFolderName]; + if ([[NSFileManager defaultManager] fileExistsAtPath:patchesFolderPath]) { + NSError *removeError = nil; + BOOL patchesFolderRemoved = [[NSFileManager defaultManager] removeItemAtPath:patchesFolderPath + error:&removeError]; + if (!patchesFolderRemoved) { + if (error) { + *error = removeError; + } + return NO; + } + } + + return YES; +} + #pragma mark - Public methods + (void)clearUpdates @@ -114,10 +189,12 @@ + (void)downloadPackage:(NSDictionary *)updatePackage NSString *diffManifestFilePath = [unzippedFolderPath stringByAppendingPathComponent:DiffManifestFileName]; BOOL isDiffUpdate = [[NSFileManager defaultManager] fileExistsAtPath:diffManifestFilePath]; - + CodePushDiffManifest *diffManifest = nil; + NSString *currentPackageFolderPath = nil; + if (isDiffUpdate) { // Copy the current package to the new package. - NSString *currentPackageFolderPath = [self getCurrentPackageFolderPath:&error]; + currentPackageFolderPath = [self getCurrentPackageFolderPath:&error]; if (error) { failCallback(error); return; @@ -160,7 +237,6 @@ + (void)downloadPackage:(NSDictionary *)updatePackage } } - // Delete files mentioned in the manifest. NSString *manifestContent = [NSString stringWithContentsOfFile:diffManifestFilePath encoding:NSUTF8StringEncoding error:&error]; @@ -178,12 +254,20 @@ + (void)downloadPackage:(NSDictionary *)updatePackage return; } - CodePushDiffManifest *diffManifest = [CodePushDiffManifest manifestFromJSON:manifestJSON error:&error]; + diffManifest = [CodePushDiffManifest manifestFromJSON:manifestJSON error:&error]; if (error) { failCallback(error); return; } + if (![CodePushPackage validateDiffManifest:diffManifest + currentPackageFolder:currentPackageFolderPath + enableDeltaUpdates:[[CodePushConfig current] enableDeltaUpdates] + error:&error]) { + failCallback(error); + return; + } + for (NSString *deletedFileName in diffManifest.deletedFiles) { // deletedFiles comes from the downloaded update, so it is untrusted: an // entry that does not name a file inside the new package folder, such as @@ -221,7 +305,21 @@ + (void)downloadPackage:(NSDictionary *)updatePackage failCallback(error); return; } - + + if (isDiffUpdate) { + // Run patching after both copyItemAtPath:currentPackageFolderPath (old-package + // bytes, above) and copyEntriesInFolder (downloaded-tree bytes, immediately + // above) so patched output overwrites bytes copied in by either at the same paths. + if (![CodePushPackage applyDiffManifest:diffManifest + currentPackageFolder:currentPackageFolderPath + unzippedFolder:unzippedFolderPath + newUpdateFolder:newUpdateFolderPath + error:&error]) { + failCallback(error); + return; + } + } + [[NSFileManager defaultManager] removeItemAtPath:unzippedFolderPath error:&nonFailingError]; if (nonFailingError) {