Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/setup-ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,19 @@ MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANkWYydPuyOumR/sn2agNBVDnzyRpM16NAUpYPGxNgjSEp0e
</plist>
```

### 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
<plist version="1.0">
<dict>
<!-- ...other configs... -->

<key>CodePushEnableDeltaUpdates</key>
<true/>

<!-- ...other configs... -->
</dict>
</plist>
```
1 change: 1 addition & 0 deletions ios/CodePush/CodePush.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
@property (copy) NSString *deploymentKey;
@property (copy) NSString *serverURL;
@property (copy) NSString *publicKey;
@property (readonly) BOOL enableDeltaUpdates;

+ (instancetype)current;

Expand Down
3 changes: 2 additions & 1 deletion ios/CodePush/CodePushConfig.m
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
108 changes: 103 additions & 5 deletions ios/CodePush/CodePushPackage.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "CodePush.h"
#import "CodePushDiffManifest.h"
#import "CodePushErrorUtils.h"
#import "CodePushBinaryDiffPatcher.h"
#if __has_include(<SSZipArchive/SSZipArchive.h>)
#import <SSZipArchive/SSZipArchive.h>
#else
Expand All @@ -12,13 +13,87 @@ @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";
static NSString *const UpdateBundleFileName = @"app.jsbundle";
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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version and flag validation depend on nothing, but the parsed manifest. They run after the copies have been done. You could split the method and run validations right after manifestFromJSON making rejections happen before the copies are done.

@ofalvai ofalvai Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rejecting an invalid manifest before any of the package copy operations is a way bigger refactor (see below), so I'd rather not include that in this PR. But I agree about splitting the validation code into its own function, so I just did that.

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) {
Comment thread
ofalvai marked this conversation as resolved.
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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -160,7 +237,6 @@ + (void)downloadPackage:(NSDictionary *)updatePackage
}
}

// Delete files mentioned in the manifest.
NSString *manifestContent = [NSString stringWithContentsOfFile:diffManifestFilePath
encoding:NSUTF8StringEncoding
error:&error];
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Loading