@revopush/react-native-code-push 2.5.0 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.claude/settings.local.json +5 -1
  2. package/README.md +2 -1
  3. package/android/build.gradle +12 -1
  4. package/android/codepush.gradle +21 -44
  5. package/android/gradle.properties +2 -0
  6. package/android/proguard-rules.pro +18 -2
  7. package/android/settings.gradle +8 -9
  8. package/android/src/main/java/com/microsoft/codepush/react/BaseBundleStore.java +78 -0
  9. package/android/src/main/java/com/microsoft/codepush/react/CodePush.java +15 -4
  10. package/android/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +2 -0
  11. package/android/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +74 -18
  12. package/android/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +56 -11
  13. package/android/src/main/java/com/microsoft/codepush/react/FileUtils.java +7 -5
  14. package/docs/setup-android.md +27 -1
  15. package/ios/CodePush/CodePush.h +14 -0
  16. package/ios/CodePush/CodePush.m +17 -44
  17. package/ios/CodePush/CodePushBaseBundleStore.m +104 -0
  18. package/ios/CodePush/CodePushPackage.m +82 -9
  19. package/ios/CodePush/CodePushUpdateUtils.m +14 -9
  20. package/ios/CodePush.xcodeproj/project.pbxproj +6 -0
  21. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/DiffUpdates +0 -0
  22. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/Headers/DiffUpdates.h +18 -0
  23. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/Headers/hpatch_objc.h +23 -0
  24. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/Info.plist +0 -0
  25. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/DiffUpdates +0 -0
  26. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/Headers/DiffUpdates.h +18 -0
  27. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/Headers/hpatch_objc.h +23 -0
  28. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/Info.plist +0 -0
  29. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/_CodeSignature/CodeDirectory +0 -0
  30. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/_CodeSignature/CodeResources +12 -9
  31. package/package.json +10 -14
  32. package/tsconfig.json +1 -4
  33. package/typings/react-native-code-push.d.ts +18 -0
  34. package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/_CodeSignature/CodeRequirements-1 +0 -0
  35. package/scripts/postlink/android/postlink.js +0 -87
  36. package/scripts/postlink/ios/postlink.js +0 -116
  37. package/scripts/postlink/run.js +0 -11
  38. package/scripts/postunlink/android/postunlink.js +0 -74
  39. package/scripts/postunlink/ios/postunlink.js +0 -87
  40. package/scripts/postunlink/run.js +0 -11
  41. package/scripts/tools/linkToolsAndroid.js +0 -57
  42. package/scripts/tools/linkToolsIos.js +0 -130
@@ -43,6 +43,10 @@ public class CodePushUpdateManager {
43
43
  return appendPathComponent(getCodePushPath(), CodePushConstants.UNZIPPED_FOLDER_NAME);
44
44
  }
45
45
 
46
+ private BaseBundleStore getBaseBundleStore() {
47
+ return new BaseBundleStore(appendPathComponent(getCodePushPath(), CodePushConstants.BASES_FOLDER_NAME));
48
+ }
49
+
46
50
  private String getDocumentsDirectory() {
47
51
  return mDocumentsDirectory;
48
52
  }
@@ -94,22 +98,22 @@ public class CodePushUpdateManager {
94
98
  }
95
99
 
96
100
  public String getCurrentPackageBundlePath(String bundleFileName) {
97
- String packageFolder = getCurrentPackageFolderPath();
98
- if (packageFolder == null) {
101
+ return getPackageBundlePath(getCurrentPackageHash(), bundleFileName);
102
+ }
103
+
104
+ private String getPackageBundlePath(String packageHash, String bundleFileName) {
105
+ if (packageHash == null) {
99
106
  return null;
100
107
  }
101
108
 
102
- JSONObject currentPackage = getCurrentPackage();
103
- if (currentPackage == null) {
109
+ JSONObject packageMetadata = getPackage(packageHash);
110
+ if (packageMetadata == null) {
104
111
  return null;
105
112
  }
106
113
 
107
- String relativeBundlePath = currentPackage.optString(CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, null);
108
- if (relativeBundlePath == null) {
109
- return appendPathComponent(packageFolder, bundleFileName);
110
- } else {
111
- return appendPathComponent(packageFolder, relativeBundlePath);
112
- }
114
+ String relativeBundlePath = packageMetadata.optString(CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, null);
115
+ return appendPathComponent(getPackageFolderPath(packageHash),
116
+ relativeBundlePath != null ? relativeBundlePath : bundleFileName);
113
117
  }
114
118
 
115
119
  public String getPackageFolderPath(String packageHash) {
@@ -154,7 +158,47 @@ public class CodePushUpdateManager {
154
158
  }
155
159
  }
156
160
 
161
+ /** A base bundle on disk, and the package hash it is stored under. */
162
+ private static final class BaseBundle {
163
+ final File file;
164
+ final String packageHash;
165
+
166
+ BaseBundle(File file, String packageHash) {
167
+ this.file = file;
168
+ this.packageHash = packageHash;
169
+ }
170
+ }
171
+
172
+ /** The base named by basePackage: the saved copy, else that package's own bundle. */
173
+ private BaseBundle findBaseBundle(JSONObject updatePackage, String expectedBundleFileName) {
174
+ JSONObject basePackage = updatePackage.optJSONObject(CodePushConstants.BASE_PACKAGE_KEY);
175
+ if (basePackage == null) {
176
+ return null;
177
+ }
178
+ String packageHash = basePackage.optString(CODE_PUSH_PACKAGE_HASH, null);
179
+ if (packageHash == null || packageHash.isEmpty()) {
180
+ // Named a base with no hash: server and SDK disagree on the field name.
181
+ CodePushUtils.log("basePackage has no packageHash; patching against the binary.");
182
+ return null;
183
+ }
184
+ File bundle = getBaseBundleStore().lookup(packageHash);
185
+ if (bundle == null) {
186
+ String bundlePath = getPackageBundlePath(packageHash, expectedBundleFileName);
187
+ bundle = bundlePath == null ? null : new File(bundlePath);
188
+ }
189
+ if (bundle == null || !bundle.isFile()) {
190
+ CodePushUtils.log("Base " + packageHash + " not held; patching against the binary.");
191
+ return null;
192
+ }
193
+ return new BaseBundle(bundle, packageHash);
194
+ }
195
+
157
196
  public void downloadPackage(JSONObject updatePackage, String expectedBundleFileName, DownloadProgressCallback progressCallback, String stringPublicKey) throws IOException {
197
+ BaseBundle base = findBaseBundle(updatePackage, expectedBundleFileName);
198
+ if (base != null) {
199
+ getBaseBundleStore().save(base.file, base.packageHash);
200
+ }
201
+
158
202
  UpdatePackage updatePack = fromJson(updatePackage);
159
203
 
160
204
  String newUpdateFolderPath = getPackageFolderPath(updatePack.getNewUpdateHash());
@@ -164,7 +208,8 @@ public class CodePushUpdateManager {
164
208
  expectedBundleFileName, // exp bundle filename
165
209
  stringPublicKey, // pub key
166
210
  updatePack.getNewUpdateHash(), // new update hash
167
- getCurrentPackage() // current package
211
+ getCurrentPackage(), // current package
212
+ base == null ? null : base.file.getAbsolutePath() // base bundle
168
213
  );
169
214
 
170
215
 
@@ -27,11 +27,13 @@ public class FileUtils {
27
27
  public static void deleteFileOrFolderSilently(File file) {
28
28
  if (file.isDirectory()) {
29
29
  File[] files = file.listFiles();
30
- for (File fileEntry : files) {
31
- if (fileEntry.isDirectory()) {
32
- deleteFileOrFolderSilently(fileEntry);
33
- } else {
34
- fileEntry.delete();
30
+ if (files != null) {
31
+ for (File fileEntry : files) {
32
+ if (fileEntry.isDirectory()) {
33
+ deleteFileOrFolderSilently(fileEntry);
34
+ } else {
35
+ fileEntry.delete();
36
+ }
35
37
  }
36
38
  }
37
39
  }
@@ -18,7 +18,7 @@ In order to integrate CodePush into your Android project, please perform the fol
18
18
 
19
19
  2. Update the `MainApplication` file to use CodePush via the following changes:
20
20
 
21
- For React Native 0.76 and above: update the `MainApplication.kt`
21
+ For React Native 0.76 - 0.82: update the `MainApplication.kt`
22
22
 
23
23
  **Important! : PackageList must be instantiated only one in application lifetime.**
24
24
 
@@ -45,6 +45,32 @@ In order to integrate CodePush into your Android project, please perform the fol
45
45
  }
46
46
  ```
47
47
 
48
+ For React Native 0.83 and above: update the `MainApplication.kt`
49
+
50
+ ```kotlin
51
+ ...
52
+ // 1. Import the plugin class.
53
+ import com.microsoft.codepush.react.CodePush
54
+
55
+ class MainApplication : Application(), ReactApplication {
56
+
57
+ override val reactHost: ReactHost by lazy {
58
+ getDefaultReactHost(
59
+ context = applicationContext,
60
+ packageList =
61
+ PackageList(this).packages.apply {
62
+ // Packages that cannot be autolinked yet can be added manually here, for example:
63
+ // add(MyReactNativePackage())
64
+ },
65
+ // 2. Override the jsBundleFilePath in order to let
66
+ // the CodePush runtime determine where to get the JS
67
+ // bundle location from on each app start
68
+ jsBundleFilePath = CodePush.getJSBundleFile(),
69
+ )
70
+ }
71
+ }
72
+ ```
73
+
48
74
 
49
75
  3. Add the Deployment key to `strings.xml`:
50
76
 
@@ -136,6 +136,7 @@
136
136
  error:(NSError **)error;
137
137
 
138
138
  + (NSString *)getPackageFolderPath:(NSString *)packageHash;
139
+ + (NSString *)getCodePushPath;
139
140
 
140
141
  + (BOOL)installPackage:(NSDictionary *)updatePackage
141
142
  removePendingUpdate:(BOOL)removePendingUpdate
@@ -149,6 +150,19 @@
149
150
 
150
151
  @end
151
152
 
153
+ /**
154
+ * Bundles kept as diff bases, named by the package hash of the release they came from.
155
+ * A base a package folder no longer holds is still findable here.
156
+ */
157
+ @interface CodePushBaseBundleStore : NSObject
158
+
159
+ /** Path of the retained bundle for this package hash, or nil if the store does not hold it. */
160
+ + (NSString *)lookup:(NSString *)packageHash;
161
+
162
+ + (BOOL)save:(NSString *)sourceBundlePath forPackageHash:(NSString *)packageHash;
163
+
164
+ @end
165
+
152
166
  @interface CodePushTelemetryManager : NSObject
153
167
 
154
168
  + (NSDictionary *)getBinaryUpdateReport:(NSString *)appVersion;
@@ -19,7 +19,7 @@
19
19
 
20
20
  #import "CodePush.h"
21
21
 
22
- @interface CodePush () <RCTBridgeModule, RCTFrameUpdateObserver>
22
+ @interface CodePush () <RCTBridgeModule>
23
23
  @end
24
24
 
25
25
  @implementation CodePush {
@@ -32,8 +32,8 @@
32
32
 
33
33
  // Used to coordinate the dispatching of download progress events to JS.
34
34
  long long _latestExpectedContentLength;
35
- long long _latestReceivedConentLength;
36
- BOOL _didUpdateProgress;
35
+ long long _latestReceivedContentLength;
36
+ NSTimeInterval _lastProgressEmitTimestamp;
37
37
 
38
38
  BOOL _allowed;
39
39
  BOOL _restartInProgress;
@@ -263,18 +263,6 @@ static NSString *const LatestRollbackCountKey = @"count";
263
263
  #pragma mark - Private API methods
264
264
 
265
265
  @synthesize methodQueue = _methodQueue;
266
- @synthesize pauseCallback = _pauseCallback;
267
- @synthesize paused = _paused;
268
-
269
- - (void)setPaused:(BOOL)paused
270
- {
271
- if (_paused != paused) {
272
- _paused = paused;
273
- if (_pauseCallback) {
274
- _pauseCallback();
275
- }
276
- }
277
- }
278
266
 
279
267
  /*
280
268
  * This method is used to clear updates that are installed
@@ -340,7 +328,7 @@ static NSString *const LatestRollbackCountKey = @"count";
340
328
  @"totalBytes" : [NSNumber
341
329
  numberWithLongLong:_latestExpectedContentLength],
342
330
  @"receivedBytes" : [NSNumber
343
- numberWithLongLong:_latestReceivedConentLength]
331
+ numberWithLongLong:_latestReceivedContentLength]
344
332
  }];
345
333
  }
346
334
 
@@ -404,7 +392,6 @@ static NSString *const LatestRollbackCountKey = @"count";
404
392
  #ifdef DEBUG
405
393
  [self clearDebugUpdates];
406
394
  #endif
407
- self.paused = YES;
408
395
  NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
409
396
  NSDictionary *pendingUpdate = [preferences objectForKey:PendingUpdateKey];
410
397
  if (pendingUpdate) {
@@ -546,7 +533,7 @@ static NSString *const LatestRollbackCountKey = @"count";
546
533
  // file (since Chrome wouldn't support it). Otherwise, update
547
534
  // the current bundle URL to point at the latest update
548
535
  if ([CodePush isUsingTestConfiguration] || ![super.bridge.bundleURL.scheme hasPrefix:@"http"]) {
549
- [super.bridge setValue:[CodePush bundleURL] forKey:@"bundleURL"];
536
+ RCTReloadCommandSetBundleURL([CodePush bundleURL]);
550
537
  }
551
538
 
552
539
  RCTTriggerReloadCommandListeners(@"react-native-code-push: Restart");
@@ -727,10 +714,7 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
727
714
  }
728
715
 
729
716
  if (notifyProgress) {
730
- // Set up and unpause the frame observer so that it can emit
731
- // progress events every frame if the progress is updated.
732
- _didUpdateProgress = NO;
733
- self.paused = NO;
717
+ _lastProgressEmitTimestamp = 0;
734
718
  }
735
719
 
736
720
  NSString * publicKey = [[CodePushConfig current] publicKey];
@@ -742,17 +726,21 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
742
726
  operationQueue:_methodQueue
743
727
  // The download is progressing forward
744
728
  progressCallback:^(long long expectedContentLength, long long receivedContentLength) {
745
- // Update the download progress so that the frame observer can notify the JS side
729
+ if (!notifyProgress) {
730
+ return;
731
+ }
732
+
746
733
  _latestExpectedContentLength = expectedContentLength;
747
- _latestReceivedConentLength = receivedContentLength;
748
- _didUpdateProgress = YES;
734
+ _latestReceivedContentLength = receivedContentLength;
749
735
 
750
- // If the download is completed, stop observing frame
751
- // updates and synchronously send the last event.
752
736
  if (expectedContentLength == receivedContentLength) {
753
- _didUpdateProgress = NO;
754
- self.paused = YES;
755
737
  [self dispatchDownloadProgressEvent];
738
+ } else {
739
+ NSTimeInterval timestamp = [[NSDate date] timeIntervalSince1970];
740
+ if (timestamp - _lastProgressEmitTimestamp > 0.3) {
741
+ _lastProgressEmitTimestamp = timestamp;
742
+ [self dispatchDownloadProgressEvent];
743
+ }
756
744
  }
757
745
  }
758
746
  // The download completed
@@ -773,9 +761,6 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
773
761
  [self saveFailedUpdate:mutableUpdatePackage];
774
762
  }
775
763
 
776
- // Stop observing frame updates if the download fails.
777
- _didUpdateProgress = NO;
778
- self.paused = YES;
779
764
  reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
780
765
  }];
781
766
  }
@@ -1128,16 +1113,4 @@ RCT_EXPORT_METHOD(saveStatusReportForRetry:(NSDictionary *)statusReport)
1128
1113
  [CodePushTelemetryManager saveStatusReportForRetry:statusReport];
1129
1114
  }
1130
1115
 
1131
- #pragma mark - RCTFrameUpdateObserver Methods
1132
-
1133
- - (void)didUpdateFrame:(RCTFrameUpdate *)update
1134
- {
1135
- if (!_didUpdateProgress) {
1136
- return;
1137
- }
1138
-
1139
- [self dispatchDownloadProgressEvent];
1140
- _didUpdateProgress = NO;
1141
- }
1142
-
1143
1116
  @end
@@ -0,0 +1,104 @@
1
+ #import "CodePush.h"
2
+
3
+ // CPLog
4
+ #import <DiffUpdates/DiffUpdates.h>
5
+
6
+ @implementation CodePushBaseBundleStore
7
+
8
+ static NSString *const BasesFolderName = @"bases";
9
+ static const NSUInteger MaxSavedBases = 3;
10
+
11
+ + (NSString *)lookup:(NSString *)packageHash
12
+ {
13
+ NSString *path = [self pathForPackageHash:packageHash];
14
+ return path && [[NSFileManager defaultManager] fileExistsAtPath:path] ? path : nil;
15
+ }
16
+
17
+ /**
18
+ * Copies the bundle into the store so it outlives its package folder: installPackage deletes the
19
+ * previous package on every install, but the server can still name that release as a diff base.
20
+ * Never fails an update - a base we could not save is simply one we do not hold.
21
+ */
22
+ + (BOOL)save:(NSString *)sourceBundlePath forPackageHash:(NSString *)packageHash
23
+ {
24
+ NSFileManager *fileManager = [NSFileManager defaultManager];
25
+ NSString *destinationPath = [self pathForPackageHash:packageHash];
26
+ if (!destinationPath || ![fileManager fileExistsAtPath:sourceBundlePath]) {
27
+ return NO;
28
+ }
29
+
30
+ if ([fileManager fileExistsAtPath:destinationPath]) {
31
+ [self touch:destinationPath];
32
+ return YES;
33
+ }
34
+
35
+ // Copied aside and renamed: a copy cut short by process death must not leave a truncated file
36
+ // under the hash, which lookup would then hand out forever.
37
+ NSString *tempPath = [destinationPath stringByAppendingPathExtension:@"tmp"];
38
+ [fileManager removeItemAtPath:tempPath error:nil];
39
+
40
+ NSError *error = nil;
41
+ if (![fileManager createDirectoryAtPath:[self basesFolderPath] withIntermediateDirectories:YES attributes:nil error:&error]
42
+ || ![fileManager copyItemAtPath:sourceBundlePath toPath:tempPath error:&error]
43
+ || ![fileManager moveItemAtPath:tempPath toPath:destinationPath error:&error]) {
44
+ CPLog(@"Could not save base bundle %@: %@", packageHash, error);
45
+ [fileManager removeItemAtPath:tempPath error:nil];
46
+ return NO;
47
+ }
48
+
49
+ [self touch:destinationPath];
50
+ [self evict];
51
+ return YES;
52
+ }
53
+
54
+ + (NSString *)basesFolderPath
55
+ {
56
+ return [[CodePushPackage getCodePushPath] stringByAppendingPathComponent:BasesFolderName];
57
+ }
58
+
59
+ /** An entry is named by its hash alone, so a hash naming anything else is refused. */
60
+ + (NSString *)pathForPackageHash:(NSString *)packageHash
61
+ {
62
+ if (packageHash.length == 0
63
+ || [packageHash isEqualToString:@".."]
64
+ || ![packageHash isEqualToString:packageHash.lastPathComponent]) {
65
+ return nil;
66
+ }
67
+
68
+ return [[self basesFolderPath] stringByAppendingPathComponent:packageHash];
69
+ }
70
+
71
+ // A copy keeps the source's timestamp and reuse has to count as use, so the store stamps every
72
+ // save itself. evict orders by this.
73
+ + (void)touch:(NSString *)path
74
+ {
75
+ [[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate: [NSDate date]}
76
+ ofItemAtPath:path
77
+ error:nil];
78
+ }
79
+
80
+ /** Keeps the MaxSavedBases most recently saved entries. */
81
+ + (void)evict
82
+ {
83
+ NSFileManager *fileManager = [NSFileManager defaultManager];
84
+ NSArray<NSURL *> *entries = [fileManager contentsOfDirectoryAtURL:[NSURL fileURLWithPath:[self basesFolderPath]]
85
+ includingPropertiesForKeys:@[NSURLContentModificationDateKey]
86
+ options:0
87
+ error:nil];
88
+ if (entries.count <= MaxSavedBases) {
89
+ return;
90
+ }
91
+
92
+ NSArray<NSURL *> *oldestFirst = [entries sortedArrayUsingComparator:^(NSURL *a, NSURL *b) {
93
+ NSDate *dateA = nil, *dateB = nil;
94
+ [a getResourceValue:&dateA forKey:NSURLContentModificationDateKey error:nil];
95
+ [b getResourceValue:&dateB forKey:NSURLContentModificationDateKey error:nil];
96
+ return [dateA compare:dateB];
97
+ }];
98
+
99
+ for (NSUInteger i = 0; i < oldestFirst.count - MaxSavedBases; i++) {
100
+ [fileManager removeItemAtURL:oldestFirst[i] error:nil];
101
+ }
102
+ }
103
+
104
+ @end
@@ -90,12 +90,26 @@ static NSString *const UnzippedFolderName = @"unzipped";
90
90
  NSString *singleUrl = updatePackage[@"downloadUrl"] ?: updatePackage[@"download_url"];
91
91
 
92
92
  __block BOOL cameFromDiffHandler = (bundleDiffUrl.length > 0);
93
-
93
+ __block BOOL fallbackStarted = NO;
94
+ // Assigned below. commonDone reaches back into it to retry a diff that verified badly.
95
+ __block void (^startFullDownload)(void) = nil;
94
96
 
95
97
  void (^commonDone)(BOOL) = ^(BOOL isZip) {
96
98
  NSError *error = nil;
97
99
  NSString * unzippedFolderPath = [CodePushPackage getUnzippedFolderPath];
98
100
  NSMutableDictionary * mutableUpdatePackage = [updatePackage mutableCopy];
101
+
102
+ // A diff patched against the wrong base still produces a well-formed folder, just one
103
+ // holding the wrong bytes. Retry as a full download rather than failing the update.
104
+ BOOL (^recoverFromIntegrityFailure)(void) = ^BOOL {
105
+ if (!cameFromDiffHandler || fallbackStarted || singleUrl.length == 0) {
106
+ return NO;
107
+ }
108
+ CPLog(@"Falling back to a full download.");
109
+ startFullDownload();
110
+ return YES;
111
+ };
112
+
99
113
  if (isZip) {
100
114
  if ([[NSFileManager defaultManager] fileExistsAtPath:unzippedFolderPath]) {
101
115
  // This removes any unzipped download data that could have been left
@@ -257,6 +271,9 @@ static NSString *const UnzippedFolderName = @"unzipped";
257
271
  expectedHash:newUpdateHash
258
272
  error:&error]) {
259
273
  CPLog(@"The update contents failed the data integrity check.");
274
+ if (recoverFromIntegrityFailure()) {
275
+ return;
276
+ }
260
277
  if (!error) {
261
278
  error = [CodePushErrorUtils errorWithMessage:@"The update contents failed the data integrity check."];
262
279
  }
@@ -305,6 +322,9 @@ static NSString *const UnzippedFolderName = @"unzipped";
305
322
  expectedHash:newUpdateHash
306
323
  error:&error]) {
307
324
  CPLog(@"The update contents failed the data integrity check.");
325
+ if (recoverFromIntegrityFailure()) {
326
+ return;
327
+ }
308
328
  if (!error) {
309
329
  error = [CodePushErrorUtils errorWithMessage:@"The update contents failed the data integrity check."];
310
330
  }
@@ -347,15 +367,16 @@ static NSString *const UnzippedFolderName = @"unzipped";
347
367
  }
348
368
  };
349
369
 
350
- __block BOOL fallbackStarted = NO;
351
-
352
- void (^startFullDownload)(void) = ^{
370
+ startFullDownload = ^{
353
371
  if (fallbackStarted) return;
354
372
  fallbackStarted = YES;
355
373
 
356
374
  NSError *cleanupErr = nil;
357
375
  [[NSFileManager defaultManager] removeItemAtPath:downloadFilePath error:&cleanupErr];
358
376
  [[NSFileManager defaultManager] removeItemAtPath:[CodePushPackage getUnzippedFolderPath] error:nil];
377
+ // A diff that got as far as the integrity check already assembled this folder. Whatever it
378
+ // left behind is wrong, and copyEntriesInFolder overlays rather than replaces.
379
+ [[NSFileManager defaultManager] removeItemAtPath:newUpdateFolderPath error:nil];
359
380
 
360
381
  CodePushDownloadHandler *downloadHandler = [[CodePushDownloadHandler alloc]
361
382
  init:downloadFilePath
@@ -378,8 +399,12 @@ static NSString *const UnzippedFolderName = @"unzipped";
378
399
 
379
400
 
380
401
  if (cameFromDiffHandler) {
381
- NSString *assetsUrl = updatePackage[@"assetDownloadUrl"] ?: updatePackage[@"asset_download_url"];
382
- NSString *assetHash = updatePackage[@"assetHash"] ?: updatePackage[@"asset_hash"];
402
+ // Retained now: this is the last moment we know the release naming it is still on disk.
403
+ NSString *baseHash = [self basePackageHash:updatePackage];
404
+ NSString *baseBundlePath = [self findBaseBundleForPackageHash:baseHash];
405
+ if (baseBundlePath) {
406
+ [CodePushBaseBundleStore save:baseBundlePath forPackageHash:baseHash];
407
+ }
383
408
 
384
409
  NSError *pkgErr = nil;
385
410
  NSDictionary *currentPackage = [CodePushPackage getCurrentPackage:&pkgErr];
@@ -396,10 +421,13 @@ static NSString *const UnzippedFolderName = @"unzipped";
396
421
 
397
422
  CPLog(@"Diff download handler");
398
423
 
399
- [diffHandler downloadWithDiffUrl:bundleDiffUrl assetsUrl:assetsUrl assetHash:assetHash nativeAssetHash:nativeAssetHash currentPackage:currentPackage];
424
+ [diffHandler downloadWithDiffUrl:bundleDiffUrl
425
+ assetsUrl:assetsUrl
426
+ assetHash:assetHash
427
+ nativeAssetHash:nativeAssetHash
428
+ currentPackage:currentPackage
429
+ baseBundlePath:baseBundlePath];
400
430
  } else {
401
- NSString *singleUrl = updatePackage[@"downloadUrl"] ?: updatePackage[@"download_url"];
402
-
403
431
  CodePushDownloadHandler *downloadHandler = [[CodePushDownloadHandler alloc]
404
432
  init:downloadFilePath
405
433
  operationQueue:operationQueue
@@ -540,6 +568,51 @@ static NSString *const UnzippedFolderName = @"unzipped";
540
568
  return [[self getCodePushPath] stringByAppendingPathComponent:packageHash];
541
569
  }
542
570
 
571
+ /** The package hash the server named as the diff base, or nil if it named none we can use. */
572
+ + (NSString *)basePackageHash:(NSDictionary *)updatePackage
573
+ {
574
+ NSDictionary *basePackage = updatePackage[@"basePackage"];
575
+ if (![basePackage isKindOfClass:NSDictionary.class]) {
576
+ return nil;
577
+ }
578
+
579
+ NSString *packageHash = basePackage[@"packageHash"];
580
+ if (packageHash.length == 0) {
581
+ // Named a base with no hash: server and SDK disagree on the field name.
582
+ CPLog(@"basePackage has no packageHash. Patching against the binary.");
583
+ return nil;
584
+ }
585
+
586
+ return packageHash;
587
+ }
588
+
589
+ /**
590
+ * The bundle to patch against: the retained copy of that release, else the release's own bundle
591
+ * while its package folder still exists. nil means patch against the binary's bundle instead.
592
+ */
593
+ + (NSString *)findBaseBundleForPackageHash:(NSString *)packageHash
594
+ {
595
+ if (packageHash.length == 0) {
596
+ return nil;
597
+ }
598
+
599
+ NSString *bundlePath = [CodePushBaseBundleStore lookup:packageHash];
600
+ if (bundlePath) {
601
+ return bundlePath;
602
+ }
603
+
604
+ NSString *relativeBundlePath = [self getPackage:packageHash error:nil][RelativeBundlePathKey];
605
+ bundlePath = relativeBundlePath.length > 0
606
+ ? [[self getPackageFolderPath:packageHash] stringByAppendingPathComponent:relativeBundlePath]
607
+ : nil;
608
+ if (bundlePath && [[NSFileManager defaultManager] fileExistsAtPath:bundlePath]) {
609
+ return bundlePath;
610
+ }
611
+
612
+ CPLog(@"Base %@ not held. Patching against the binary.", packageHash);
613
+ return nil;
614
+ }
615
+
543
616
  + (NSDictionary *)getPreviousPackage:(NSError **)error
544
617
  {
545
618
  NSString *packageHash = [self getPreviousPackageHash:error];
@@ -197,16 +197,24 @@ NSString * const IgnoreCodePushMetadata = @".codepushrelease";
197
197
  return AssetsFolderName;
198
198
  }
199
199
 
200
+ + (NSString *)cacheKeyForBinaryAtURL:(NSURL *)binaryBundleUrl
201
+ {
202
+ NSString *bundleModifiedDate = [self modifiedDateStringOfFileAtURL:binaryBundleUrl];
203
+ return [NSString stringWithFormat:@"%@:%@",
204
+ [[NSBundle mainBundle] bundlePath],
205
+ bundleModifiedDate ?: @"unknown"];
206
+ }
207
+
200
208
  + (NSString *)getHashForBinaryContents:(NSURL *)binaryBundleUrl
201
209
  error:(NSError **)error
202
210
  {
203
211
  // Get the cached hash from user preferences if it exists.
204
- NSString *binaryModifiedDate = [self modifiedDateStringOfFileAtURL:binaryBundleUrl];
212
+ NSString *binaryCacheKey = [self cacheKeyForBinaryAtURL:binaryBundleUrl];
205
213
  NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
206
214
  NSMutableDictionary *binaryHashDictionary = [preferences objectForKey:BinaryHashKey];
207
215
  NSString *binaryHash = nil;
208
216
  if (binaryHashDictionary != nil) {
209
- binaryHash = [binaryHashDictionary objectForKey:binaryModifiedDate];
217
+ binaryHash = [binaryHashDictionary objectForKey:binaryCacheKey];
210
218
  if (binaryHash == nil) {
211
219
  [preferences removeObjectForKey:BinaryHashKey];
212
220
  [preferences synchronize];
@@ -237,9 +245,7 @@ NSString * const IgnoreCodePushMetadata = @".codepushrelease";
237
245
 
238
246
  binaryHash = [self computeFinalHashFromManifest:manifest error:error];
239
247
 
240
- // Cache the hash in user preferences. This assumes that the modified date for the
241
- // JS bundle changes every time a new bundle is generated by the packager.
242
- [binaryHashDictionary setObject:binaryHash forKey:binaryModifiedDate];
248
+ [binaryHashDictionary setObject:binaryHash forKey:binaryCacheKey];
243
249
  [preferences setObject:binaryHashDictionary forKey:BinaryHashKey];
244
250
  [preferences synchronize];
245
251
  return binaryHash;
@@ -255,11 +261,10 @@ NSString * const IgnoreCodePushMetadata = @".codepushrelease";
255
261
  NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
256
262
  NSMutableDictionary *assetsHashDictionary = [preferences objectForKey:BinaryAssetsHashKey];
257
263
 
258
- NSString *assetsModifiedDate = [self modifiedDateStringOfFileAtURL:
259
- [NSURL fileURLWithPath:assetsPath isDirectory:YES]];
264
+ NSString *assetsCacheKey = [self cacheKeyForBinaryAtURL:[CodePush binaryBundleURL]];
260
265
 
261
266
  if (assetsHashDictionary != nil) {
262
- NSString *cached = [assetsHashDictionary objectForKey:assetsModifiedDate];
267
+ NSString *cached = [assetsHashDictionary objectForKey:assetsCacheKey];
263
268
  if (cached != nil) {
264
269
  return cached;
265
270
  } else {
@@ -285,7 +290,7 @@ NSString * const IgnoreCodePushMetadata = @".codepushrelease";
285
290
  return nil;
286
291
  }
287
292
 
288
- [assetsHashDictionary setObject:assetsHash forKey:assetsModifiedDate];
293
+ [assetsHashDictionary setObject:assetsHash forKey:assetsCacheKey];
289
294
  [preferences setObject:assetsHashDictionary forKey:BinaryAssetsHashKey];
290
295
  [preferences synchronize];
291
296