@bravemobile/react-native-code-push 13.0.0 → 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 (105) hide show
  1. package/CodePush.podspec +35 -3
  2. package/README.md +115 -3
  3. package/THIRD-PARTY-NOTICES +96 -0
  4. package/android/app/build.gradle +19 -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 +1 -1
  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 +12 -2
  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 +48 -1
  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 +57 -1
  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 +18 -1
  94. package/ios/CodePush/CodePush.mm +16 -1
  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/package.json +10 -3
  100. package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
  101. package/scripts/export-embedded-bundle.sh +76 -0
  102. package/src/CodePush.js +46 -3
  103. package/src/CodePush.test.js +382 -0
  104. package/src/package-mixins.js +20 -2
  105. package/typings/react-native-code-push.d.ts +77 -0
@@ -86,6 +86,19 @@ public class CodePushUpdateUtils {
86
86
  return String.format("%064x", new java.math.BigInteger(1, hash));
87
87
  }
88
88
 
89
+ /**
90
+ * SHA-256 of a single file's bytes, as lowercase hex - the way a binary patch manifest
91
+ * records the bundle it was computed against and the bundle it produces.
92
+ */
93
+ public static String computeHashForFile(File file) throws IOException {
94
+ return computeHash(new FileInputStream(file));
95
+ }
96
+
97
+ /** SHA-256 of the bytes in memory, in the same form as {@link #computeHashForFile}. */
98
+ public static String computeHashForBytes(byte[] data) {
99
+ return computeHash(new ByteArrayInputStream(data));
100
+ }
101
+
89
102
  public static void copyNecessaryFilesFromCurrentPackage(String diffManifestFilePath, String currentPackageFolderPath, String newPackageFolderPath) throws IOException {
90
103
  if (currentPackageFolderPath == null || !new File(currentPackageFolderPath).exists()) {
91
104
  CodePushUtils.log("Unable to copy files from current package during diff update, because currentPackageFolderPath is invalid.");
@@ -0,0 +1,52 @@
1
+ package com.microsoft.codepush.react;
2
+
3
+ /**
4
+ * The native HDiffPatch applier, which is what actually turns the bundle inside the app
5
+ * binary into the bundle an update wants to run.
6
+ *
7
+ * The library is loaded on first use rather than when the class is loaded, and a load
8
+ * failure is reported as a result code instead of an error: an app whose build did not
9
+ * produce the library still has to install its updates, it just has to download them in
10
+ * full.
11
+ */
12
+ public class HDiffPatchNative implements CodePushBinaryPatch.PatchApplier {
13
+
14
+ private static final String LIBRARY_NAME = "codepush-binarypatch";
15
+
16
+ private static boolean sLibraryLoadAttempted = false;
17
+ private static boolean sLibraryLoaded = false;
18
+
19
+ @Override
20
+ public int apply(byte[] base, byte[] patch, String outputPath, long expectedTargetSize) {
21
+ if (!loadLibrary()) {
22
+ return RESULT_LIBRARY_UNAVAILABLE;
23
+ }
24
+
25
+ return applyPatch(base, patch, outputPath, expectedTargetSize);
26
+ }
27
+
28
+ private static synchronized boolean loadLibrary() {
29
+ if (!sLibraryLoadAttempted) {
30
+ sLibraryLoadAttempted = true;
31
+ try {
32
+ System.loadLibrary(LIBRARY_NAME);
33
+ sLibraryLoaded = true;
34
+ } catch (UnsatisfiedLinkError e) {
35
+ CodePushUtils.log("Unable to load the binary patch library: " + e.getMessage());
36
+ }
37
+ }
38
+
39
+ return sLibraryLoaded;
40
+ }
41
+
42
+ /**
43
+ * Writes the bundle that `patch` produces from `base` to `outputPath`.
44
+ *
45
+ * The base bundle and the patch are passed as arrays because the applier needs random
46
+ * access to both, while the restored bundle is written straight to a file, so the two
47
+ * bundles are never in memory at the same time.
48
+ *
49
+ * @return one of the RESULT_* codes of {@link CodePushBinaryPatch.PatchApplier}
50
+ */
51
+ private static native int applyPatch(byte[] base, byte[] patch, String outputPath, long expectedTargetSize);
52
+ }
@@ -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
+ }