@bravemobile/react-native-code-push 12.4.1 → 13.1.0-beta.0

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 (111) hide show
  1. package/CodePush.podspec +47 -2
  2. package/README.md +118 -3
  3. package/THIRD-PARTY-NOTICES +96 -0
  4. package/android/app/build.gradle +26 -0
  5. package/android/app/proguard-rules.pro +5 -0
  6. package/android/app/src/main/cpp/CMakeLists.txt +38 -0
  7. package/android/app/src/main/cpp/binarypatch_jni.c +181 -0
  8. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +92 -0
  9. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchResult.java +64 -0
  10. package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +46 -11
  11. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +293 -0
  12. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +25 -0
  13. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +79 -88
  14. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +159 -4
  15. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +13 -0
  16. package/android/app/src/main/java/com/microsoft/codepush/react/HDiffPatchNative.java +52 -0
  17. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +393 -0
  18. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +207 -0
  19. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +449 -0
  20. package/android/codepush-export.gradle +115 -0
  21. package/cli/commands/bundleCommand/bundleCodePush.ts +39 -7
  22. package/cli/commands/bundleCommand/index.ts +9 -1
  23. package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
  24. package/cli/commands/releaseCommand/index.test.ts +118 -0
  25. package/cli/commands/releaseCommand/index.ts +20 -2
  26. package/cli/commands/releaseCommand/release.test.ts +582 -0
  27. package/cli/commands/releaseCommand/release.ts +232 -15
  28. package/cli/dist/commands/bundleCommand/bundleCodePush.js +26 -7
  29. package/cli/dist/commands/bundleCommand/index.js +5 -1
  30. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
  31. package/cli/dist/commands/releaseCommand/index.js +8 -1
  32. package/cli/dist/commands/releaseCommand/index.test.js +96 -0
  33. package/cli/dist/commands/releaseCommand/release.js +165 -17
  34. package/cli/dist/commands/releaseCommand/release.test.js +367 -0
  35. package/cli/dist/functions/makeBinaryPatchBundle.js +248 -0
  36. package/cli/dist/functions/makeBinaryPatchBundle.test.js +321 -0
  37. package/cli/dist/functions/makeCodePushBundle.js +4 -2
  38. package/cli/dist/functions/runHermesEmitBinaryCommand.js +98 -27
  39. package/cli/dist/functions/runHermesEmitBinaryCommand.test.js +100 -0
  40. package/cli/dist/jest.globalSetup.js +38 -0
  41. package/cli/dist/utils/binaryPatch.js +86 -0
  42. package/cli/dist/utils/binaryPatch.test.js +190 -0
  43. package/cli/functions/makeBinaryPatchBundle.test.ts +410 -0
  44. package/cli/functions/makeBinaryPatchBundle.ts +367 -0
  45. package/cli/functions/makeCodePushBundle.ts +4 -2
  46. package/cli/functions/runHermesEmitBinaryCommand.test.ts +124 -0
  47. package/cli/functions/runHermesEmitBinaryCommand.ts +112 -26
  48. package/cli/jest.globalSetup.ts +43 -0
  49. package/cli/utils/binaryPatch.test.ts +234 -0
  50. package/cli/utils/binaryPatch.ts +103 -0
  51. package/cpp/binarypatch/binarypatch_zstd_decompressor.c +187 -0
  52. package/cpp/binarypatch/binarypatch_zstd_decompressor.h +47 -0
  53. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/checksum_plugin.h +52 -0
  54. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/hpatch_mt/hpatch_mt.h +147 -0
  55. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c +3315 -0
  56. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.h +268 -0
  57. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_private.h +297 -0
  58. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_types.h +434 -0
  59. package/cpp/binarypatch/vendor/zstd/common/allocations.h +55 -0
  60. package/cpp/binarypatch/vendor/zstd/common/bits.h +205 -0
  61. package/cpp/binarypatch/vendor/zstd/common/bitstream.h +454 -0
  62. package/cpp/binarypatch/vendor/zstd/common/compiler.h +464 -0
  63. package/cpp/binarypatch/vendor/zstd/common/cpu.h +249 -0
  64. package/cpp/binarypatch/vendor/zstd/common/debug.c +30 -0
  65. package/cpp/binarypatch/vendor/zstd/common/debug.h +107 -0
  66. package/cpp/binarypatch/vendor/zstd/common/entropy_common.c +340 -0
  67. package/cpp/binarypatch/vendor/zstd/common/error_private.c +64 -0
  68. package/cpp/binarypatch/vendor/zstd/common/error_private.h +158 -0
  69. package/cpp/binarypatch/vendor/zstd/common/fse.h +625 -0
  70. package/cpp/binarypatch/vendor/zstd/common/fse_decompress.c +315 -0
  71. package/cpp/binarypatch/vendor/zstd/common/huf.h +277 -0
  72. package/cpp/binarypatch/vendor/zstd/common/mem.h +422 -0
  73. package/cpp/binarypatch/vendor/zstd/common/pool.c +371 -0
  74. package/cpp/binarypatch/vendor/zstd/common/pool.h +81 -0
  75. package/cpp/binarypatch/vendor/zstd/common/portability_macros.h +171 -0
  76. package/cpp/binarypatch/vendor/zstd/common/threading.c +182 -0
  77. package/cpp/binarypatch/vendor/zstd/common/threading.h +142 -0
  78. package/cpp/binarypatch/vendor/zstd/common/xxhash.c +18 -0
  79. package/cpp/binarypatch/vendor/zstd/common/xxhash.h +7094 -0
  80. package/cpp/binarypatch/vendor/zstd/common/zstd_common.c +48 -0
  81. package/cpp/binarypatch/vendor/zstd/common/zstd_deps.h +123 -0
  82. package/cpp/binarypatch/vendor/zstd/common/zstd_internal.h +324 -0
  83. package/cpp/binarypatch/vendor/zstd/common/zstd_trace.h +156 -0
  84. package/cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c +1944 -0
  85. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c +244 -0
  86. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.h +44 -0
  87. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c +2410 -0
  88. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c +2209 -0
  89. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.h +73 -0
  90. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_internal.h +240 -0
  91. package/cpp/binarypatch/vendor/zstd/zstd.h +3206 -0
  92. package/cpp/binarypatch/vendor/zstd/zstd_errors.h +107 -0
  93. package/ios/CodePush/CodePush.h +58 -9
  94. package/ios/CodePush/{CodePush.m → CodePush.mm} +77 -63
  95. package/ios/CodePush/CodePushBinaryPatch.h +80 -0
  96. package/ios/CodePush/CodePushBinaryPatch.m +473 -0
  97. package/ios/CodePush/CodePushPackage.m +183 -2
  98. package/ios/CodePush/CodePushUpdateUtils.m +49 -1
  99. package/ios/CodePush.xcodeproj/project.pbxproj +6 -6
  100. package/package.json +28 -4
  101. package/react-native.config.js +3 -1
  102. package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
  103. package/scripts/export-embedded-bundle.sh +76 -0
  104. package/src/CodePush.js +190 -96
  105. package/src/CodePush.test.js +382 -0
  106. package/src/native/NativeCodePush.ts +162 -0
  107. package/src/package-mixins.js +24 -11
  108. package/src/specs/NativeCodePush.ts +43 -0
  109. package/typings/react-native-code-push.d.ts +77 -0
  110. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java +0 -102
  111. package/src/AlertAdapter.js +0 -24
@@ -0,0 +1,393 @@
1
+ package com.microsoft.codepush.react;
2
+
3
+ import static org.junit.Assert.assertArrayEquals;
4
+ import static org.junit.Assert.assertEquals;
5
+ import static org.junit.Assert.assertFalse;
6
+ import static org.junit.Assert.assertTrue;
7
+
8
+ import org.json.JSONObject;
9
+ import org.junit.Before;
10
+ import org.junit.Rule;
11
+ import org.junit.Test;
12
+ import org.junit.rules.TemporaryFolder;
13
+
14
+ import java.io.File;
15
+ import java.io.FileInputStream;
16
+ import java.io.FileOutputStream;
17
+ import java.io.IOException;
18
+ import java.io.InputStream;
19
+ import java.io.OutputStream;
20
+ import java.nio.charset.Charset;
21
+ import java.security.MessageDigest;
22
+ import java.security.NoSuchAlgorithmException;
23
+
24
+ public class CodePushBinaryPatchTest {
25
+
26
+ private static final String BUNDLE_FILE_NAME = "index.android.bundle";
27
+ private static final String PATCH_FILE_NAME = BUNDLE_FILE_NAME + ".patch";
28
+
29
+ private static final byte[] BASE_BUNDLE = bytes("the bundle inside the app binary");
30
+ private static final byte[] TARGET_BUNDLE = bytes("the bundle the update wants to run");
31
+ private static final byte[] PATCH = bytes("the difference between the two");
32
+
33
+ @Rule
34
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
35
+
36
+ private File mContentsFolder;
37
+ private File mWorkingFolder;
38
+ private File mManifestFile;
39
+ private File mPatchFile;
40
+ private File mBundleFile;
41
+
42
+ @Before
43
+ public void setUp() throws IOException {
44
+ mContentsFolder = mTemporaryFolder.newFolder("unzipped");
45
+ mWorkingFolder = new File(mTemporaryFolder.getRoot(), CodePushConstants.BINARY_PATCH_FOLDER_NAME);
46
+ mManifestFile = new File(mContentsFolder, CodePushConstants.BINARY_PATCH_MANIFEST_FILE_NAME);
47
+ mPatchFile = new File(mContentsFolder, PATCH_FILE_NAME);
48
+ mBundleFile = new File(mContentsFolder, BUNDLE_FILE_NAME);
49
+
50
+ writeFile(mPatchFile, PATCH);
51
+ writeManifest(validManifest());
52
+ }
53
+
54
+ @Test
55
+ public void appliesAPatchAndLeavesTheContentsOfAFullArchiveBehind() throws IOException {
56
+ FakePatchApplier applier = new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, TARGET_BUNDLE);
57
+
58
+ BinaryPatchResult result = restoreBundle(BASE_BUNDLE, applier);
59
+
60
+ assertTrue(result.succeeded());
61
+ assertArrayEquals(TARGET_BUNDLE, readFile(mBundleFile));
62
+ assertFalse("the patch file is not part of the update", mPatchFile.exists());
63
+ assertFalse("the manifest is not part of the update", mManifestFile.exists());
64
+ assertArrayEquals(BASE_BUNDLE, applier.base);
65
+ assertArrayEquals(PATCH, applier.patch);
66
+ assertEquals(TARGET_BUNDLE.length, applier.expectedTargetSize);
67
+ assertNoTemporaryFilesLeft();
68
+ }
69
+
70
+ @Test
71
+ public void appliesAPatchThatSitsInTheArchivesOwnRootDirectory() throws IOException {
72
+ // An archive wraps its files in one directory, and the manifest's paths are relative
73
+ // to that directory rather than to the unzipped archive.
74
+ File unzippedFolder = mTemporaryFolder.newFolder("archive");
75
+ File archiveRoot = new File(unzippedFolder, "CodePush");
76
+ assertTrue(archiveRoot.mkdirs());
77
+ assertTrue(mManifestFile.renameTo(new File(archiveRoot, CodePushConstants.BINARY_PATCH_MANIFEST_FILE_NAME)));
78
+ assertTrue(mPatchFile.renameTo(new File(archiveRoot, PATCH_FILE_NAME)));
79
+
80
+ BinaryPatchResult result = new CodePushBinaryPatch(providerOf(BASE_BUNDLE), succeedingApplier())
81
+ .restoreBundle(unzippedFolder.getAbsolutePath(), mWorkingFolder.getAbsolutePath(), BUNDLE_FILE_NAME);
82
+
83
+ assertTrue(result.succeeded());
84
+ assertArrayEquals(TARGET_BUNDLE, readFile(new File(archiveRoot, BUNDLE_FILE_NAME)));
85
+ assertFalse(new File(archiveRoot, PATCH_FILE_NAME).exists());
86
+ assertFalse(new File(archiveRoot, CodePushConstants.BINARY_PATCH_MANIFEST_FILE_NAME).exists());
87
+ assertNoTemporaryFilesLeft();
88
+ }
89
+
90
+ @Test
91
+ public void removesWhatAnInterruptedAttemptLeftInTheWorkingDirectory() throws IOException {
92
+ assertTrue(mWorkingFolder.mkdirs());
93
+ writeFile(new File(mWorkingFolder, CodePushConstants.BINARY_PATCH_TARGET_FILE_NAME), bytes("half a bundle"));
94
+
95
+ BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
96
+ new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, TARGET_BUNDLE));
97
+
98
+ assertTrue(result.succeeded());
99
+ assertArrayEquals(TARGET_BUNDLE, readFile(mBundleFile));
100
+ assertNoTemporaryFilesLeft();
101
+ }
102
+
103
+ @Test
104
+ public void reportsAnInvalidManifestWhenThereIsNone() {
105
+ assertTrue(mManifestFile.delete());
106
+
107
+ assertFailure(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
108
+ }
109
+
110
+ @Test
111
+ public void reportsAnInvalidManifestWhenItIsNotJson() throws IOException {
112
+ writeFile(mManifestFile, bytes("not json at all"));
113
+
114
+ assertFailure(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
115
+ }
116
+
117
+ @Test
118
+ public void reportsAnUnsupportedFormatForAnotherFormatVersion() throws IOException {
119
+ writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_FORMAT_VERSION_KEY,
120
+ CodePushConstants.BINARY_PATCH_FORMAT_VERSION + 1));
121
+
122
+ assertFailure(BinaryPatchResult.REASON_UNSUPPORTED_FORMAT, restoreBundle(BASE_BUNDLE, succeedingApplier()));
123
+ }
124
+
125
+ @Test
126
+ public void reportsAnUnsupportedFormatForAnotherAlgorithm() throws IOException {
127
+ writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_ALGORITHM_KEY, "bsdiff-bz2"));
128
+
129
+ assertFailure(BinaryPatchResult.REASON_UNSUPPORTED_FORMAT, restoreBundle(BASE_BUNDLE, succeedingApplier()));
130
+ }
131
+
132
+ @Test
133
+ public void reportsAnInvalidManifestWhenTheBundlePathLeavesTheArchive() throws IOException {
134
+ writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_BUNDLE_PATH_KEY,
135
+ "../" + BUNDLE_FILE_NAME));
136
+
137
+ assertFailure(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
138
+ assertFalse(new File(mTemporaryFolder.getRoot(), BUNDLE_FILE_NAME).exists());
139
+ }
140
+
141
+ @Test
142
+ public void reportsAnInvalidManifestWhenThePatchPathLeavesTheArchive() throws IOException {
143
+ writeFile(new File(mTemporaryFolder.getRoot(), PATCH_FILE_NAME), PATCH);
144
+ writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_FILE_KEY, "../" + PATCH_FILE_NAME));
145
+
146
+ assertFailure(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
147
+ }
148
+
149
+ @Test
150
+ public void reportsAnInvalidManifestWhenTheBundlePathIsAbsolute() throws IOException {
151
+ writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_BUNDLE_PATH_KEY,
152
+ new File(mTemporaryFolder.getRoot(), BUNDLE_FILE_NAME).getAbsolutePath()));
153
+
154
+ assertFailure(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
155
+ }
156
+
157
+ @Test
158
+ public void reportsAnInvalidManifestWhenTheArchiveHasNoPatchFile() {
159
+ assertTrue(mPatchFile.delete());
160
+
161
+ assertFailure(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
162
+ }
163
+
164
+ @Test
165
+ public void reportsAnInvalidManifestWhenTheTargetSizeIsEmpty() throws IOException {
166
+ writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_TARGET_BUNDLE_SIZE_KEY, 0));
167
+
168
+ assertFailure(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
169
+ }
170
+
171
+ @Test
172
+ public void reportsAnInvalidManifestWhenTheTargetSizeIsBeyondTheLimit() throws IOException {
173
+ writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_TARGET_BUNDLE_SIZE_KEY,
174
+ CodePushConstants.BINARY_PATCH_MAX_TARGET_BUNDLE_SIZE + 1));
175
+
176
+ assertFailure(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
177
+ }
178
+
179
+ @Test
180
+ public void reportsAnUnavailableBaseBundleWhenTheBinaryBundleCannotBeRead() {
181
+ CodePushBinaryPatch binaryPatch = new CodePushBinaryPatch(new CodePushBinaryPatch.BaseBundleProvider() {
182
+ @Override
183
+ public byte[] readBaseBundle(String bundleFileName) throws IOException {
184
+ throw new IOException("no such asset: " + bundleFileName);
185
+ }
186
+ }, succeedingApplier());
187
+
188
+ assertFailure(BinaryPatchResult.REASON_BASE_BUNDLE_UNAVAILABLE, restoreBundle(binaryPatch));
189
+ }
190
+
191
+ @Test
192
+ public void reportsABaseHashMismatchWhenTheBinaryHoldsAnotherBundle() {
193
+ FakePatchApplier applier = new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, TARGET_BUNDLE);
194
+
195
+ BinaryPatchResult result = restoreBundle(bytes("a bundle from another app build"), applier);
196
+
197
+ assertFailure(BinaryPatchResult.REASON_BASE_HASH_MISMATCH, result);
198
+ assertEquals("a patch is not applied to a base it was not computed against", 0, applier.invocationCount);
199
+ }
200
+
201
+ @Test
202
+ public void reportsAnUnsupportedFormatWhenTheApplierRejectsTheCodec() {
203
+ BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
204
+ new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_UNSUPPORTED_COMPRESSION, null));
205
+
206
+ assertFailure(BinaryPatchResult.REASON_UNSUPPORTED_FORMAT, result);
207
+ }
208
+
209
+ @Test
210
+ public void reportsAFailedApplyWhenThePatchHeaderIsCorrupt() {
211
+ BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
212
+ new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_INVALID_HEADER, null));
213
+
214
+ assertFailure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED, result);
215
+ assertNoRestoredBundleInTheContents();
216
+ }
217
+
218
+ @Test
219
+ public void reportsAFailedApplyWhenTheNativeLibraryIsMissing() {
220
+ BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
221
+ new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_LIBRARY_UNAVAILABLE, null));
222
+
223
+ assertFailure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED, result);
224
+ }
225
+
226
+ @Test
227
+ public void reportsAFailedVerificationWhenTheRestoredBundleHasAnotherSize() {
228
+ BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
229
+ new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, bytes("too short")));
230
+
231
+ assertFailure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED, result);
232
+ assertNoRestoredBundleInTheContents();
233
+ }
234
+
235
+ @Test
236
+ public void reportsAFailedVerificationWhenTheRestoredBundleHasAnotherContent() {
237
+ // A corrupted patch body applies without any error and produces wrong bytes, which
238
+ // only the target hash catches.
239
+ byte[] wrongBundle = TARGET_BUNDLE.clone();
240
+ wrongBundle[0] = (byte) (wrongBundle[0] ^ 0xFF);
241
+
242
+ BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
243
+ new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, wrongBundle));
244
+
245
+ assertFailure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED, result);
246
+ assertNoRestoredBundleInTheContents();
247
+ }
248
+
249
+ private BinaryPatchResult restoreBundle(byte[] baseBundle, CodePushBinaryPatch.PatchApplier applier) {
250
+ return restoreBundle(new CodePushBinaryPatch(providerOf(baseBundle), applier));
251
+ }
252
+
253
+ private BinaryPatchResult restoreBundle(CodePushBinaryPatch binaryPatch) {
254
+ return binaryPatch.restoreBundle(mContentsFolder.getAbsolutePath(), mWorkingFolder.getAbsolutePath(),
255
+ BUNDLE_FILE_NAME);
256
+ }
257
+
258
+ private void assertFailure(String expectedReason, BinaryPatchResult result) {
259
+ assertFalse(result.succeeded());
260
+ assertEquals(expectedReason, result.getFailureReason());
261
+ assertNoTemporaryFilesLeft();
262
+ }
263
+
264
+ private void assertNoTemporaryFilesLeft() {
265
+ assertFalse("the working directory outlived the patch attempt", mWorkingFolder.exists());
266
+ }
267
+
268
+ /** The update must never be installed from bytes that did not pass verification. */
269
+ private void assertNoRestoredBundleInTheContents() {
270
+ assertFalse(mBundleFile.exists());
271
+ assertTrue(mPatchFile.exists());
272
+ assertTrue(mManifestFile.exists());
273
+ }
274
+
275
+ private JSONObject validManifest() {
276
+ JSONObject manifest = new JSONObject();
277
+ putValue(manifest, CodePushConstants.BINARY_PATCH_FORMAT_VERSION_KEY, CodePushConstants.BINARY_PATCH_FORMAT_VERSION);
278
+ putValue(manifest, CodePushConstants.BINARY_PATCH_ALGORITHM_KEY, CodePushConstants.BINARY_PATCH_ALGORITHM);
279
+ putValue(manifest, CodePushConstants.BINARY_PATCH_BUNDLE_PATH_KEY, BUNDLE_FILE_NAME);
280
+ putValue(manifest, CodePushConstants.BINARY_PATCH_FILE_KEY, PATCH_FILE_NAME);
281
+ putValue(manifest, CodePushConstants.BINARY_PATCH_BASE_BUNDLE_HASH_KEY, sha256(BASE_BUNDLE));
282
+ putValue(manifest, CodePushConstants.BINARY_PATCH_TARGET_BUNDLE_HASH_KEY, sha256(TARGET_BUNDLE));
283
+ putValue(manifest, CodePushConstants.BINARY_PATCH_TARGET_BUNDLE_SIZE_KEY, TARGET_BUNDLE.length);
284
+ return manifest;
285
+ }
286
+
287
+ private static JSONObject putValue(JSONObject manifest, String key, Object value) {
288
+ CodePushUtils.setJSONValueForKey(manifest, key, value);
289
+ return manifest;
290
+ }
291
+
292
+ private void writeManifest(JSONObject manifest) throws IOException {
293
+ writeFile(mManifestFile, bytes(manifest.toString()));
294
+ }
295
+
296
+ private static CodePushBinaryPatch.BaseBundleProvider providerOf(final byte[] baseBundle) {
297
+ return new CodePushBinaryPatch.BaseBundleProvider() {
298
+ @Override
299
+ public byte[] readBaseBundle(String bundleFileName) {
300
+ assertEquals(BUNDLE_FILE_NAME, bundleFileName);
301
+ return baseBundle;
302
+ }
303
+ };
304
+ }
305
+
306
+ private static CodePushBinaryPatch.PatchApplier succeedingApplier() {
307
+ return new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, TARGET_BUNDLE);
308
+ }
309
+
310
+ /** Stands in for the native applier, which is exercised on a device instead. */
311
+ private static class FakePatchApplier implements CodePushBinaryPatch.PatchApplier {
312
+
313
+ private final int mResultCode;
314
+ private final byte[] mRestoredBundle;
315
+
316
+ int invocationCount;
317
+ byte[] base;
318
+ byte[] patch;
319
+ long expectedTargetSize;
320
+
321
+ FakePatchApplier(int resultCode, byte[] restoredBundle) {
322
+ mResultCode = resultCode;
323
+ mRestoredBundle = restoredBundle;
324
+ }
325
+
326
+ @Override
327
+ public int apply(byte[] base, byte[] patch, String outputPath, long expectedTargetSize) {
328
+ this.invocationCount++;
329
+ this.base = base;
330
+ this.patch = patch;
331
+ this.expectedTargetSize = expectedTargetSize;
332
+
333
+ if (mRestoredBundle != null) {
334
+ try {
335
+ writeFile(new File(outputPath), mRestoredBundle);
336
+ } catch (IOException e) {
337
+ return RESULT_IO_ERROR;
338
+ }
339
+ }
340
+
341
+ return mResultCode;
342
+ }
343
+ }
344
+
345
+ private static void writeFile(File file, byte[] contents) throws IOException {
346
+ OutputStream output = new FileOutputStream(file);
347
+ try {
348
+ output.write(contents);
349
+ } finally {
350
+ output.close();
351
+ }
352
+ }
353
+
354
+ private static byte[] readFile(File file) throws IOException {
355
+ byte[] contents = new byte[(int) file.length()];
356
+ InputStream input = new FileInputStream(file);
357
+ try {
358
+ int offset = 0;
359
+ while (offset < contents.length) {
360
+ int bytesRead = input.read(contents, offset, contents.length - offset);
361
+ if (bytesRead < 0) {
362
+ throw new IOException("Unexpected end of " + file);
363
+ }
364
+ offset += bytesRead;
365
+ }
366
+ } finally {
367
+ input.close();
368
+ }
369
+
370
+ return contents;
371
+ }
372
+
373
+ private static byte[] bytes(String text) {
374
+ return text.getBytes(Charset.forName("UTF-8"));
375
+ }
376
+
377
+ /** Hashes the way a manifest does, without going through the code under test. */
378
+ private static String sha256(byte[] data) {
379
+ MessageDigest digest;
380
+ try {
381
+ digest = MessageDigest.getInstance("SHA-256");
382
+ } catch (NoSuchAlgorithmException e) {
383
+ throw new IllegalStateException(e);
384
+ }
385
+
386
+ StringBuilder hex = new StringBuilder();
387
+ for (byte hashByte : digest.digest(data)) {
388
+ hex.append(String.format("%02x", hashByte));
389
+ }
390
+
391
+ return hex.toString();
392
+ }
393
+ }
@@ -0,0 +1,207 @@
1
+ package com.microsoft.codepush.react;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+ import static org.junit.Assert.assertFalse;
5
+ import static org.junit.Assert.assertNull;
6
+ import static org.junit.Assert.assertTrue;
7
+
8
+ import org.json.JSONObject;
9
+ import org.junit.Before;
10
+ import org.junit.Rule;
11
+ import org.junit.Test;
12
+ import org.junit.rules.TemporaryFolder;
13
+
14
+ import java.io.File;
15
+ import java.io.IOException;
16
+ import java.util.ArrayList;
17
+ import java.util.Arrays;
18
+ import java.util.List;
19
+
20
+ /**
21
+ * Which archive an update is downloaded from, and how the one download that is allowed to
22
+ * fail falls back to the other one.
23
+ */
24
+ public class CodePushUpdateManagerBinaryPatchTest {
25
+
26
+ private static final String BUNDLE_FILE_NAME = "index.android.bundle";
27
+ private static final String FULL_ARCHIVE_URL = "https://example.test/updates/full.zip";
28
+ private static final String PATCH_ARCHIVE_URL = "https://example.test/updates/full.zip-patch.zip";
29
+ /** Long enough that a timing that wrongly included it could not be mistaken for one that did not. */
30
+ private static final long FULL_ARCHIVE_DOWNLOAD_DURATION_MS = 300;
31
+
32
+ @Rule
33
+ public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
34
+
35
+ private String mDocumentsDirectory;
36
+ private File mBinaryPatchFolder;
37
+
38
+ @Before
39
+ public void setUp() {
40
+ mDocumentsDirectory = mTemporaryFolder.getRoot().getAbsolutePath();
41
+ mBinaryPatchFolder = new File(
42
+ new File(mDocumentsDirectory, CodePushConstants.CODE_PUSH_FOLDER_PREFIX),
43
+ CodePushConstants.BINARY_PATCH_FOLDER_NAME);
44
+ }
45
+
46
+ @Test
47
+ public void downloadsTheFullArchiveWhenTheUpdateHasNoBinaryPatch() throws IOException {
48
+ RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
49
+ BinaryPatchResult.success());
50
+
51
+ JSONObject patchResult = updateManager.downloadPackage(updatePackage(null), BUNDLE_FILE_NAME, ignoreProgress());
52
+
53
+ assertEquals(Arrays.asList(FULL_ARCHIVE_URL), updateManager.downloadedUrls);
54
+ assertEquals(Arrays.asList(false), updateManager.patchAttempts);
55
+ assertNull("a download with no patch to try has nothing to report about one", patchResult);
56
+ }
57
+
58
+ @Test
59
+ public void downloadsOnlyTheBinaryPatchArchiveWhenThePatchApplies() throws IOException {
60
+ RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
61
+ BinaryPatchResult.success());
62
+
63
+ JSONObject patchResult = updateManager.downloadPackage(updatePackage(PATCH_ARCHIVE_URL), BUNDLE_FILE_NAME,
64
+ ignoreProgress());
65
+
66
+ assertEquals(Arrays.asList(PATCH_ARCHIVE_URL), updateManager.downloadedUrls);
67
+ assertEquals(Arrays.asList(true), updateManager.patchAttempts);
68
+ assertEquals("applied", patchResult.optString("status", null));
69
+ }
70
+
71
+ @Test
72
+ public void fallsBackToTheFullArchiveOnceWhenThePatchCannotBeApplied() throws IOException {
73
+ RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
74
+ BinaryPatchResult.failure(BinaryPatchResult.REASON_BASE_HASH_MISMATCH));
75
+
76
+ JSONObject patchResult = updateManager.downloadPackage(updatePackage(PATCH_ARCHIVE_URL), BUNDLE_FILE_NAME,
77
+ ignoreProgress());
78
+
79
+ assertEquals(Arrays.asList(PATCH_ARCHIVE_URL, FULL_ARCHIVE_URL), updateManager.downloadedUrls);
80
+ // The second download is not a patch download, so it has no patch of its own to fail
81
+ // at: the fallback can only happen the once.
82
+ assertEquals(Arrays.asList(true, false), updateManager.patchAttempts);
83
+ assertEquals("fallback", patchResult.optString("status", null));
84
+ // The reason travels to the app exactly as the applier worded it.
85
+ assertEquals(BinaryPatchResult.REASON_BASE_HASH_MISMATCH, patchResult.optString("fallbackReason", null));
86
+ }
87
+
88
+ @Test
89
+ public void fallsBackToTheFullArchiveWhenTheBinaryPatchArchiveCannotBeDownloaded() throws IOException {
90
+ RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory, null);
91
+
92
+ JSONObject patchResult = updateManager.downloadPackage(updatePackage(PATCH_ARCHIVE_URL), BUNDLE_FILE_NAME,
93
+ ignoreProgress());
94
+
95
+ assertEquals(Arrays.asList(PATCH_ARCHIVE_URL, FULL_ARCHIVE_URL), updateManager.downloadedUrls);
96
+ assertEquals(Arrays.asList(true, false), updateManager.patchAttempts);
97
+ assertEquals("fallback", patchResult.optString("status", null));
98
+ // An archive that could not be downloaded is not something the appliers have a word
99
+ // for, and no word is invented for it here.
100
+ assertFalse(patchResult.has("fallbackReason"));
101
+ }
102
+
103
+ @Test
104
+ public void timesThePatchAttemptRatherThanTheDownloadThatFollowedIt() throws IOException {
105
+ RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
106
+ BinaryPatchResult.failure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED));
107
+ updateManager.fullArchiveDownloadDurationMs = FULL_ARCHIVE_DOWNLOAD_DURATION_MS;
108
+
109
+ JSONObject patchResult = updateManager.downloadPackage(updatePackage(PATCH_ARCHIVE_URL), BUNDLE_FILE_NAME,
110
+ ignoreProgress());
111
+
112
+ // The attempt was over before the full archive was even asked for, so what it reports
113
+ // is time the patch spent and nothing else.
114
+ long reportedDurationMs = patchResult.optLong("applyDurationMs", -1);
115
+ assertTrue("the fallback is timed over the download that followed it: " + reportedDurationMs + " ms",
116
+ reportedDurationMs >= 0 && reportedDurationMs < FULL_ARCHIVE_DOWNLOAD_DURATION_MS);
117
+ }
118
+
119
+ @Test
120
+ public void removesTheBinaryPatchWorkingDirectoryWhateverTheOutcomeIs() throws IOException {
121
+ List<BinaryPatchResult> outcomes = Arrays.asList(BinaryPatchResult.success(),
122
+ BinaryPatchResult.failure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED), null);
123
+
124
+ for (BinaryPatchResult outcome : outcomes) {
125
+ assertTrue(mBinaryPatchFolder.mkdirs());
126
+ assertTrue(new File(mBinaryPatchFolder, CodePushConstants.BINARY_PATCH_TARGET_FILE_NAME).createNewFile());
127
+
128
+ new RecordingUpdateManager(mDocumentsDirectory, outcome)
129
+ .downloadPackage(updatePackage(PATCH_ARCHIVE_URL), BUNDLE_FILE_NAME, ignoreProgress());
130
+
131
+ assertFalse(mBinaryPatchFolder.exists());
132
+ }
133
+ }
134
+
135
+ private static JSONObject updatePackage(String binaryPatchDownloadUrl) {
136
+ JSONObject updatePackage = new JSONObject();
137
+ CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.PACKAGE_HASH_KEY, "package-hash");
138
+ CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.DOWNLOAD_URL_KEY, FULL_ARCHIVE_URL);
139
+ if (binaryPatchDownloadUrl != null) {
140
+ CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.BINARY_PATCH_DOWNLOAD_URL_KEY,
141
+ binaryPatchDownloadUrl);
142
+ }
143
+
144
+ return updatePackage;
145
+ }
146
+
147
+ private static DownloadProgressCallback ignoreProgress() {
148
+ return new DownloadProgressCallback() {
149
+ @Override
150
+ public void call(DownloadProgress downloadProgress) {
151
+ }
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Records which archive each download went to instead of downloading and installing it.
157
+ * Downloading is what these cover, so the manager is built without the collaborator that
158
+ * applies a patch - nothing here reaches it.
159
+ */
160
+ private static class RecordingUpdateManager extends CodePushUpdateManager {
161
+
162
+ private final BinaryPatchResult mPatchOutcome;
163
+
164
+ final List<String> downloadedUrls = new ArrayList<>();
165
+ final List<Boolean> patchAttempts = new ArrayList<>();
166
+
167
+ /** How long the full archive takes to download, for timings that must not include it. */
168
+ long fullArchiveDownloadDurationMs = 0;
169
+
170
+ /** @param patchOutcome what the patch download ends in, or null when it cannot be downloaded */
171
+ RecordingUpdateManager(String documentsDirectory, BinaryPatchResult patchOutcome) {
172
+ super(documentsDirectory, (CodePushBinaryPatch) null);
173
+ mPatchOutcome = patchOutcome;
174
+ }
175
+
176
+ @Override
177
+ BinaryPatchResult downloadAndInstallPackage(JSONObject updatePackage, String expectedBundleFileName,
178
+ DownloadProgressCallback progressCallback,
179
+ String downloadUrlString, boolean isBinaryPatchUpdate,
180
+ BinaryPatchAttempt patchAttempt) throws IOException {
181
+ downloadedUrls.add(downloadUrlString);
182
+ patchAttempts.add(isBinaryPatchUpdate);
183
+
184
+ if (!isBinaryPatchUpdate) {
185
+ sleep(fullArchiveDownloadDurationMs);
186
+ return BinaryPatchResult.success();
187
+ }
188
+ if (mPatchOutcome == null) {
189
+ throw new IOException("the binary patch archive is not there");
190
+ }
191
+
192
+ return mPatchOutcome;
193
+ }
194
+
195
+ private static void sleep(long durationMs) {
196
+ if (durationMs <= 0) {
197
+ return;
198
+ }
199
+
200
+ try {
201
+ Thread.sleep(durationMs);
202
+ } catch (InterruptedException e) {
203
+ Thread.currentThread().interrupt();
204
+ }
205
+ }
206
+ }
207
+ }