@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
@@ -0,0 +1,64 @@
1
+ package com.microsoft.codepush.react;
2
+
3
+ /**
4
+ * Outcome of restoring an update from its binary patch archive.
5
+ *
6
+ * A failed result is not an error the user hears about: it is the signal to download the
7
+ * update's full archive instead. The reason strings are the vocabulary the appliers of
8
+ * every platform report, and the logs a rollout is judged from are read for exactly these
9
+ * words, so they must not be reworded.
10
+ */
11
+ public class BinaryPatchResult {
12
+
13
+ /** The bundle inside the app binary could not be opened or read. */
14
+ public static final String REASON_BASE_BUNDLE_UNAVAILABLE = "base_bundle_unavailable";
15
+
16
+ /** The bundle inside the app binary is not the one the patch was computed against. */
17
+ public static final String REASON_BASE_HASH_MISMATCH = "base_hash_mismatch";
18
+
19
+ /** The manifest is missing, malformed, points outside the archive, or asks for too much. */
20
+ public static final String REASON_INVALID_MANIFEST = "invalid_manifest";
21
+
22
+ /** The patch was produced by a format or a codec this client cannot apply. */
23
+ public static final String REASON_UNSUPPORTED_FORMAT = "unsupported_format";
24
+
25
+ /** The applier refused the patch, or the restored bundle could not be written. */
26
+ public static final String REASON_PATCH_APPLY_FAILED = "patch_apply_failed";
27
+
28
+ /** The restored bundle is not the one the manifest promised. */
29
+ public static final String REASON_TARGET_VERIFICATION_FAILED = "target_verification_failed";
30
+
31
+ /**
32
+ * The update restored from the patch did not pass the checks every update passes before
33
+ * it is installed - the folder hash above all.
34
+ *
35
+ * This one is reported by the caller rather than by the applier: by the time it applies,
36
+ * the applier has already handed back a bundle it was happy with.
37
+ */
38
+ public static final String REASON_PACKAGE_VERIFICATION_FAILED = "package_verification_failed";
39
+
40
+ private final boolean mSucceeded;
41
+ private final String mFailureReason;
42
+
43
+ private BinaryPatchResult(boolean succeeded, String failureReason) {
44
+ mSucceeded = succeeded;
45
+ mFailureReason = failureReason;
46
+ }
47
+
48
+ public static BinaryPatchResult success() {
49
+ return new BinaryPatchResult(true, null);
50
+ }
51
+
52
+ public static BinaryPatchResult failure(String failureReason) {
53
+ return new BinaryPatchResult(false, failureReason);
54
+ }
55
+
56
+ public boolean succeeded() {
57
+ return mSucceeded;
58
+ }
59
+
60
+ /** Why the full archive has to be downloaded instead, or null when the patch was applied. */
61
+ public String getFailureReason() {
62
+ return mFailureReason;
63
+ }
64
+ }
@@ -61,7 +61,7 @@ public class CodePush extends TurboReactPackage {
61
61
  private CodePush(Context context, boolean isDebugMode) {
62
62
  mContext = context.getApplicationContext();
63
63
 
64
- mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath());
64
+ mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath(), mContext);
65
65
  mTelemetryManager = new CodePushTelemetryManager(mContext);
66
66
  mDeploymentKey = "deprecated_deployment_key";
67
67
  mIsDebugMode = isDebugMode;
@@ -0,0 +1,293 @@
1
+ package com.microsoft.codepush.react;
2
+
3
+ import org.json.JSONObject;
4
+
5
+ import java.io.File;
6
+ import java.io.FileInputStream;
7
+ import java.io.FileOutputStream;
8
+ import java.io.IOException;
9
+ import java.io.InputStream;
10
+ import java.io.OutputStream;
11
+
12
+ /**
13
+ * Rebuilds the JS bundle of an update that was downloaded as a binary patch archive.
14
+ *
15
+ * A patch archive holds everything the full archive holds except the JS bundle, which it
16
+ * carries as a patch against the bundle that shipped inside the app binary, plus a
17
+ * manifest describing how to rebuild it. Restoring means applying that patch, verifying
18
+ * the result, moving it to where the bundle belongs and deleting the two patch-only
19
+ * files. What is left is byte for byte the contents of the full archive, so the folder
20
+ * hash check that follows the install is unchanged and stays the last line of defence.
21
+ *
22
+ * Nothing here trusts the patch. Neither the diff format nor the zstd streams inside it
23
+ * carry a checksum of the data they produce, so an apply that reports success is not
24
+ * proof of a correct result: a base bundle of the right size but the wrong content, or a
25
+ * corrupted patch body, both produce wrong bytes without any error. The base bundle is
26
+ * hashed before the patch is applied and the restored bundle is hashed afterwards, and
27
+ * the restored bytes only reach the update contents once both checks have passed.
28
+ *
29
+ * Every failure is reported as a {@link BinaryPatchResult}, never as an exception: the
30
+ * caller answers all of them the same way, by downloading the full archive instead.
31
+ */
32
+ public class CodePushBinaryPatch {
33
+
34
+ /** Reads the JS bundle that shipped inside the app binary. */
35
+ public interface BaseBundleProvider {
36
+ byte[] readBaseBundle(String bundleFileName) throws IOException;
37
+ }
38
+
39
+ /**
40
+ * Applies a patch to a base bundle and writes the restored bundle to a file.
41
+ *
42
+ * The result codes are the ones the native applier returns, so they have to stay in
43
+ * step with the codes in the JNI wrapper.
44
+ */
45
+ public interface PatchApplier {
46
+ int RESULT_OK = 0;
47
+ int RESULT_INVALID_ARGUMENT = 1;
48
+ int RESULT_IO_ERROR = 2;
49
+ int RESULT_INVALID_HEADER = 3;
50
+ int RESULT_UNSUPPORTED_COMPRESSION = 4;
51
+ int RESULT_SIZE_MISMATCH = 5;
52
+ int RESULT_APPLY_FAILED = 6;
53
+ /** Reported by the wrapper itself, when the native library could not be loaded. */
54
+ int RESULT_LIBRARY_UNAVAILABLE = 7;
55
+
56
+ int apply(byte[] base, byte[] patch, String outputPath, long expectedTargetSize);
57
+ }
58
+
59
+ private static final int COPY_BUFFER_SIZE = 1024 * 8;
60
+
61
+ private final BaseBundleProvider mBaseBundleProvider;
62
+ private final PatchApplier mPatchApplier;
63
+
64
+ public CodePushBinaryPatch(BaseBundleProvider baseBundleProvider, PatchApplier patchApplier) {
65
+ mBaseBundleProvider = baseBundleProvider;
66
+ mPatchApplier = patchApplier;
67
+ }
68
+
69
+ /**
70
+ * Turns the contents of a downloaded patch archive into the contents of the full one.
71
+ *
72
+ * @param unzippedFolderPath the unzipped archive, which is modified in place
73
+ * @param workingFolderPath scratch directory for the restored bundle, emptied before
74
+ * and after the attempt so an interrupted run leaves nothing
75
+ * @param baseBundleFileName name of the JS bundle inside the app binary
76
+ */
77
+ public BinaryPatchResult restoreBundle(String unzippedFolderPath, String workingFolderPath, String baseBundleFileName) {
78
+ File contentsFolder = resolveContentsFolder(new File(unzippedFolderPath));
79
+ File manifestFile = new File(contentsFolder, CodePushConstants.BINARY_PATCH_MANIFEST_FILE_NAME);
80
+ if (!manifestFile.isFile()) {
81
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_INVALID_MANIFEST);
82
+ }
83
+
84
+ JSONObject manifest;
85
+ try {
86
+ manifest = CodePushUtils.getJsonObjectFromFile(manifestFile.getAbsolutePath());
87
+ } catch (IOException | CodePushMalformedDataException e) {
88
+ CodePushUtils.log(e);
89
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_INVALID_MANIFEST);
90
+ }
91
+
92
+ if (manifest.optInt(CodePushConstants.BINARY_PATCH_FORMAT_VERSION_KEY, -1) != CodePushConstants.BINARY_PATCH_FORMAT_VERSION
93
+ || !CodePushConstants.BINARY_PATCH_ALGORITHM.equals(manifest.optString(CodePushConstants.BINARY_PATCH_ALGORITHM_KEY, null))) {
94
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_UNSUPPORTED_FORMAT);
95
+ }
96
+
97
+ File targetBundleFile = resolveInsideFolder(contentsFolder, manifest.optString(CodePushConstants.BINARY_PATCH_BUNDLE_PATH_KEY, null));
98
+ File patchFile = resolveInsideFolder(contentsFolder, manifest.optString(CodePushConstants.BINARY_PATCH_FILE_KEY, null));
99
+ String baseBundleHash = manifest.optString(CodePushConstants.BINARY_PATCH_BASE_BUNDLE_HASH_KEY, null);
100
+ String targetBundleHash = manifest.optString(CodePushConstants.BINARY_PATCH_TARGET_BUNDLE_HASH_KEY, null);
101
+ long targetBundleSize = manifest.optLong(CodePushConstants.BINARY_PATCH_TARGET_BUNDLE_SIZE_KEY, -1);
102
+ if (targetBundleFile == null || patchFile == null || !patchFile.isFile()
103
+ || isNullOrEmpty(baseBundleHash) || isNullOrEmpty(targetBundleHash)
104
+ || targetBundleSize <= 0 || targetBundleSize > CodePushConstants.BINARY_PATCH_MAX_TARGET_BUNDLE_SIZE) {
105
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_INVALID_MANIFEST);
106
+ }
107
+
108
+ // An earlier attempt that was killed while patching leaves its restored bundle behind.
109
+ FileUtils.deleteDirectoryAtPath(workingFolderPath);
110
+ File workingFolder = new File(workingFolderPath);
111
+ if (!workingFolder.mkdirs()) {
112
+ CodePushUtils.log("Unable to create the binary patch working directory at " + workingFolderPath);
113
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED);
114
+ }
115
+
116
+ try {
117
+ if (workingFolder.getUsableSpace() < targetBundleSize) {
118
+ CodePushUtils.log("Not enough free space to restore a " + targetBundleSize + " byte bundle.");
119
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED);
120
+ }
121
+
122
+ byte[] baseBundle;
123
+ try {
124
+ baseBundle = mBaseBundleProvider.readBaseBundle(baseBundleFileName);
125
+ } catch (Exception e) {
126
+ CodePushUtils.log(e);
127
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_BASE_BUNDLE_UNAVAILABLE);
128
+ }
129
+ if (baseBundle == null) {
130
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_BASE_BUNDLE_UNAVAILABLE);
131
+ }
132
+ if (!baseBundleHash.equals(CodePushUpdateUtils.computeHashForBytes(baseBundle))) {
133
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_BASE_HASH_MISMATCH);
134
+ }
135
+
136
+ byte[] patch;
137
+ try {
138
+ patch = readFile(patchFile);
139
+ } catch (IOException e) {
140
+ CodePushUtils.log(e);
141
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED);
142
+ }
143
+
144
+ File restoredBundleFile = new File(workingFolder, CodePushConstants.BINARY_PATCH_TARGET_FILE_NAME);
145
+ int resultCode = mPatchApplier.apply(baseBundle, patch, restoredBundleFile.getAbsolutePath(), targetBundleSize);
146
+ if (resultCode != PatchApplier.RESULT_OK) {
147
+ CodePushUtils.log("The binary patch applier returned " + resultCode + ".");
148
+ return BinaryPatchResult.failure(reasonForResultCode(resultCode));
149
+ }
150
+
151
+ if (restoredBundleFile.length() != targetBundleSize) {
152
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED);
153
+ }
154
+ String restoredBundleHash;
155
+ try {
156
+ restoredBundleHash = CodePushUpdateUtils.computeHashForFile(restoredBundleFile);
157
+ } catch (IOException e) {
158
+ CodePushUtils.log(e);
159
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED);
160
+ }
161
+ if (!targetBundleHash.equals(restoredBundleHash)) {
162
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED);
163
+ }
164
+
165
+ if (!moveFile(restoredBundleFile, targetBundleFile) || !patchFile.delete() || !manifestFile.delete()) {
166
+ // The contents are half restored, so they must not be installed. The full
167
+ // archive that follows unzips over them, which is what clears them.
168
+ CodePushUtils.log("Unable to put the restored bundle in place of the patch.");
169
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED);
170
+ }
171
+
172
+ return BinaryPatchResult.success();
173
+ } finally {
174
+ FileUtils.deleteDirectoryAtPath(workingFolderPath);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Finds the contents root inside an unzipped archive.
180
+ *
181
+ * An archive wraps its files in a single directory, and the manifest's paths are
182
+ * relative to that directory rather than to the archive. An archive whose files are at
183
+ * the top level is its own contents root, which is how the tooling that unpacks one
184
+ * reads it too.
185
+ */
186
+ private static File resolveContentsFolder(File unzippedFolder) {
187
+ File[] entries = unzippedFolder.listFiles();
188
+ if (entries != null && entries.length == 1 && entries[0].isDirectory()) {
189
+ return entries[0];
190
+ }
191
+
192
+ return unzippedFolder;
193
+ }
194
+
195
+ /**
196
+ * Resolves a path the manifest points at, refusing anything that would reach outside
197
+ * the archive - an archive is untrusted input, and its manifest is no more trusted
198
+ * than its entries.
199
+ *
200
+ * @return the resolved file, or null when the path is unusable
201
+ */
202
+ private static File resolveInsideFolder(File folder, String relativePath) {
203
+ if (isNullOrEmpty(relativePath) || new File(relativePath).isAbsolute()) {
204
+ return null;
205
+ }
206
+
207
+ try {
208
+ String folderPath = folder.getCanonicalPath() + File.separator;
209
+ String resolvedPath = new File(folder, relativePath).getCanonicalPath();
210
+ if (!resolvedPath.startsWith(folderPath)) {
211
+ return null;
212
+ }
213
+
214
+ return new File(resolvedPath);
215
+ } catch (IOException e) {
216
+ CodePushUtils.log(e);
217
+ return null;
218
+ }
219
+ }
220
+
221
+ private static String reasonForResultCode(int resultCode) {
222
+ return resultCode == PatchApplier.RESULT_UNSUPPORTED_COMPRESSION
223
+ ? BinaryPatchResult.REASON_UNSUPPORTED_FORMAT
224
+ : BinaryPatchResult.REASON_PATCH_APPLY_FAILED;
225
+ }
226
+
227
+ /** Rename, falling back to a copy for the case where the two paths are on different volumes. */
228
+ private static boolean moveFile(File sourceFile, File destinationFile) {
229
+ File destinationFolder = destinationFile.getParentFile();
230
+ if (destinationFolder != null && !destinationFolder.exists() && !destinationFolder.mkdirs()) {
231
+ return false;
232
+ }
233
+
234
+ if (sourceFile.renameTo(destinationFile)) {
235
+ return true;
236
+ }
237
+
238
+ try {
239
+ copyFile(sourceFile, destinationFile);
240
+ } catch (IOException e) {
241
+ CodePushUtils.log(e);
242
+ return false;
243
+ }
244
+
245
+ return sourceFile.delete();
246
+ }
247
+
248
+ private static void copyFile(File sourceFile, File destinationFile) throws IOException {
249
+ InputStream input = new FileInputStream(sourceFile);
250
+ try {
251
+ OutputStream output = new FileOutputStream(destinationFile);
252
+ try {
253
+ byte[] buffer = new byte[COPY_BUFFER_SIZE];
254
+ int bytesRead;
255
+ while ((bytesRead = input.read(buffer)) > 0) {
256
+ output.write(buffer, 0, bytesRead);
257
+ }
258
+ } finally {
259
+ output.close();
260
+ }
261
+ } finally {
262
+ input.close();
263
+ }
264
+ }
265
+
266
+ private static byte[] readFile(File file) throws IOException {
267
+ long fileSize = file.length();
268
+ if (fileSize <= 0 || fileSize > Integer.MAX_VALUE) {
269
+ throw new IOException("Cannot read " + file.getAbsolutePath() + ", it is " + fileSize + " bytes long.");
270
+ }
271
+
272
+ byte[] contents = new byte[(int) fileSize];
273
+ InputStream input = new FileInputStream(file);
274
+ try {
275
+ int offset = 0;
276
+ while (offset < contents.length) {
277
+ int bytesRead = input.read(contents, offset, contents.length - offset);
278
+ if (bytesRead < 0) {
279
+ throw new IOException("Unexpected end of " + file.getAbsolutePath() + ".");
280
+ }
281
+ offset += bytesRead;
282
+ }
283
+ } finally {
284
+ input.close();
285
+ }
286
+
287
+ return contents;
288
+ }
289
+
290
+ private static boolean isNullOrEmpty(String value) {
291
+ return value == null || value.isEmpty();
292
+ }
293
+ }
@@ -2,6 +2,31 @@ package com.microsoft.codepush.react;
2
2
 
3
3
  public class CodePushConstants {
4
4
  public static final String ASSETS_BUNDLE_PREFIX = "assets://";
5
+ // The binary patch values below are the client half of the contract the CLI writes
6
+ // into a patch archive. The CLI is TypeScript, so its constants cannot be imported
7
+ // here: any change to the manifest it produces has to be repeated in this file.
8
+ public static final String BINARY_PATCH_ALGORITHM = "hdiffpatch-m-zstd";
9
+ public static final String BINARY_PATCH_ALGORITHM_KEY = "algorithm";
10
+ public static final String BINARY_PATCH_BASE_BUNDLE_HASH_KEY = "baseBundleHash";
11
+ public static final String BINARY_PATCH_BUNDLE_PATH_KEY = "bundlePath";
12
+ public static final String BINARY_PATCH_DOWNLOAD_URL_KEY = "binaryPatchDownloadUrl";
13
+ public static final String BINARY_PATCH_FILE_KEY = "patchFile";
14
+ public static final String BINARY_PATCH_FOLDER_NAME = "binary-patch";
15
+ public static final int BINARY_PATCH_FORMAT_VERSION = 1;
16
+ public static final String BINARY_PATCH_FORMAT_VERSION_KEY = "formatVersion";
17
+ public static final String BINARY_PATCH_MANIFEST_FILE_NAME = "codepush-binary-patch.json";
18
+ // A manifest asking for a bundle larger than this is treated as malformed rather than
19
+ // as a reason to reserve that much memory and disk. A large Hermes bundle stays under
20
+ // 50 MB, and a release that somehow exceeds the bound still installs from its full
21
+ // archive, so the headroom below is generous enough for the limit to only ever catch
22
+ // a manifest that is wrong. The other platform's applier holds the same value.
23
+ public static final long BINARY_PATCH_MAX_TARGET_BUNDLE_SIZE = 128L * 1024 * 1024;
24
+ // Not part of the CLI's contract: the field a download that tried a patch reports its
25
+ // outcome under, on the package it resolves with and on no package it stores.
26
+ public static final String BINARY_PATCH_RESULT_KEY = "binaryPatchResult";
27
+ public static final String BINARY_PATCH_TARGET_BUNDLE_HASH_KEY = "targetBundleHash";
28
+ public static final String BINARY_PATCH_TARGET_BUNDLE_SIZE_KEY = "targetBundleSize";
29
+ public static final String BINARY_PATCH_TARGET_FILE_NAME = "target.bundle";
5
30
  public static final String CODE_PUSH_FOLDER_PREFIX = "CodePush";
6
31
  public static final String CODE_PUSH_HASH_FILE_NAME = "CodePushHash";
7
32
  public static final String CODE_PUSH_OLD_HASH_FILE_NAME = "CodePushHash.json";
@@ -336,7 +336,7 @@ public class CodePushNativeModule extends NativeCodePushSpec {
336
336
  public void run() {
337
337
  try {
338
338
  JSONObject mutableUpdatePackage = CodePushUtils.convertReadableToJsonObject(updatePackage);
339
- mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
339
+ JSONObject binaryPatchResult = mUpdateManager.downloadPackage(mutableUpdatePackage, mCodePush.getAssetsBundleFileName(), new DownloadProgressCallback() {
340
340
  private volatile boolean hasScheduledNextFrame = false;
341
341
  private volatile DownloadProgress latestDownloadProgress = null;
342
342
  private final DownloadProgressEventDispatcher progressEventDispatcher = new DownloadProgressEventDispatcher(
@@ -384,7 +384,17 @@ public class CodePushNativeModule extends NativeCodePushSpec {
384
384
  });
385
385
 
386
386
  JSONObject newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY));
387
- promise.resolve(CodePushUtils.convertJsonObjectToWritable(newPackage));
387
+ WritableMap downloadedPackage = CodePushUtils.convertJsonObjectToWritable(newPackage);
388
+ if (binaryPatchResult != null) {
389
+ // The result describes this download rather than the update, so it is
390
+ // put on what this call resolves with and nowhere else: the metadata
391
+ // the update manager has already written knows nothing about it, and
392
+ // whoever asked for the download decides what the result is worth.
393
+ downloadedPackage.putMap(CodePushConstants.BINARY_PATCH_RESULT_KEY,
394
+ CodePushUtils.convertJsonObjectToWritable(binaryPatchResult));
395
+ }
396
+
397
+ promise.resolve(downloadedPackage);
388
398
  } catch (CodePushInvalidUpdateException e) {
389
399
  CodePushUtils.log(e);
390
400
  mSettingsManager.saveFailedUpdate(CodePushUtils.convertReadableToJsonObject(updatePackage));
@@ -1,14 +1,17 @@
1
1
  package com.microsoft.codepush.react;
2
2
 
3
+ import android.content.Context;
3
4
  import android.os.Build;
4
5
 
5
6
  import org.json.JSONObject;
6
7
 
7
8
  import java.io.BufferedInputStream;
8
9
  import java.io.BufferedOutputStream;
10
+ import java.io.ByteArrayOutputStream;
9
11
  import java.io.File;
10
12
  import java.io.FileOutputStream;
11
13
  import java.io.IOException;
14
+ import java.io.InputStream;
12
15
  import java.net.HttpURLConnection;
13
16
  import java.net.MalformedURLException;
14
17
  import java.net.URL;
@@ -19,9 +22,15 @@ import javax.net.ssl.HttpsURLConnection;
19
22
  public class CodePushUpdateManager {
20
23
 
21
24
  private String mDocumentsDirectory;
25
+ private final CodePushBinaryPatch mBinaryPatch;
22
26
 
23
- public CodePushUpdateManager(String documentsDirectory) {
27
+ public CodePushUpdateManager(String documentsDirectory, Context context) {
28
+ this(documentsDirectory, new CodePushBinaryPatch(new AssetsBaseBundleProvider(context), new HDiffPatchNative()));
29
+ }
30
+
31
+ CodePushUpdateManager(String documentsDirectory, CodePushBinaryPatch binaryPatch) {
24
32
  mDocumentsDirectory = documentsDirectory;
33
+ mBinaryPatch = binaryPatch;
25
34
  }
26
35
 
27
36
  private String getDownloadFilePath() {
@@ -32,6 +41,10 @@ public class CodePushUpdateManager {
32
41
  return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.UNZIPPED_FOLDER_NAME);
33
42
  }
34
43
 
44
+ private String getBinaryPatchFolderPath() {
45
+ return CodePushUtils.appendPathComponent(getCodePushPath(), CodePushConstants.BINARY_PATCH_FOLDER_NAME);
46
+ }
47
+
35
48
  private String getDocumentsDirectory() {
36
49
  return mDocumentsDirectory;
37
50
  }
@@ -143,8 +156,92 @@ public class CodePushUpdateManager {
143
156
  }
144
157
  }
145
158
 
146
- public void downloadPackage(JSONObject updatePackage, String expectedBundleFileName,
147
- DownloadProgressCallback progressCallback) throws IOException {
159
+ /**
160
+ * @return what the attempt at installing the update from its binary patch archive ended
161
+ * in, or null when the update had no patch archive to attempt. The result belongs
162
+ * to this download alone: the metadata written for the update never carries it.
163
+ */
164
+ public JSONObject downloadPackage(JSONObject updatePackage, String expectedBundleFileName,
165
+ DownloadProgressCallback progressCallback) throws IOException {
166
+ // A release that was published with a binary patch offers two archives of the same
167
+ // update. The patch is worth trying because it is a fraction of the size, and the
168
+ // full archive is always there when it does not work out.
169
+ String binaryPatchDownloadUrl = updatePackage.optString(CodePushConstants.BINARY_PATCH_DOWNLOAD_URL_KEY, null);
170
+ BinaryPatchAttempt patchAttempt = null;
171
+ if (binaryPatchDownloadUrl != null) {
172
+ patchAttempt = new BinaryPatchAttempt();
173
+ if (tryDownloadBinaryPatchPackage(updatePackage, expectedBundleFileName, progressCallback,
174
+ binaryPatchDownloadUrl, patchAttempt)) {
175
+ return patchAttempt.result();
176
+ }
177
+ }
178
+
179
+ downloadAndInstallPackage(updatePackage, expectedBundleFileName, progressCallback,
180
+ updatePackage.optString(CodePushConstants.DOWNLOAD_URL_KEY, null), false, null);
181
+
182
+ return patchAttempt == null ? null : patchAttempt.result();
183
+ }
184
+
185
+ /**
186
+ * Installs the update from its binary patch archive.
187
+ *
188
+ * Every way this can fail ends the same way, with the update being downloaded in full
189
+ * instead, so none of them is reported to the caller as an error. The fallback happens
190
+ * exactly once without anything having to count it: the full archive is downloaded by
191
+ * a call that is not allowed to take the patch path, so it has no failure of its own to
192
+ * fall back from.
193
+ *
194
+ * @param patchAttempt records what the attempt ended in, for the caller to hand to
195
+ * whoever asked for the download
196
+ * @return true when the update was installed, false when the caller has to download the
197
+ * full archive instead
198
+ */
199
+ private boolean tryDownloadBinaryPatchPackage(JSONObject updatePackage, String expectedBundleFileName,
200
+ DownloadProgressCallback progressCallback,
201
+ String binaryPatchDownloadUrl, BinaryPatchAttempt patchAttempt) {
202
+ try {
203
+ BinaryPatchResult patchResult = downloadAndInstallPackage(updatePackage, expectedBundleFileName,
204
+ progressCallback, binaryPatchDownloadUrl, true, patchAttempt);
205
+ if (patchResult.succeeded()) {
206
+ return true;
207
+ }
208
+
209
+ patchAttempt.recordFallback(patchResult.getFailureReason());
210
+ CodePushUtils.log("Binary patch update failed (" + patchResult.getFailureReason()
211
+ + "). Downloading the full update instead.");
212
+ } catch (Exception | OutOfMemoryError e) {
213
+ // Applying a patch is the one path that holds a whole bundle in memory, so
214
+ // running out of it is a failure this has to absorb like any other: by the time
215
+ // it lands here the arrays are unreachable, and the full archive is downloaded
216
+ // to disk in chunks rather than held.
217
+ patchAttempt.recordFallbackAfterError();
218
+ CodePushUtils.log(e);
219
+ CodePushUtils.log("The binary patch update could not be completed. Downloading the full update instead.");
220
+ } finally {
221
+ FileUtils.deleteDirectoryAtPath(getBinaryPatchFolderPath());
222
+ }
223
+
224
+ return false;
225
+ }
226
+
227
+ /**
228
+ * Downloads an update from one of its archives and installs it.
229
+ *
230
+ * @param isBinaryPatchUpdate whether the archive holds a binary patch of the JS bundle,
231
+ * which has to be applied before the contents are the update.
232
+ * Only an archive downloaded from the binary patch URL is
233
+ * treated that way, so an update being downloaded in full can
234
+ * never end up on the patch path.
235
+ * @param patchAttempt the record the patch is timed into, or null for a full download,
236
+ * which has no patch to time
237
+ * @return the outcome of the patch: a failed result means the update was not installed
238
+ * and the caller has to fall back to the full archive. Downloading the full
239
+ * archive always succeeds or throws.
240
+ */
241
+ BinaryPatchResult downloadAndInstallPackage(JSONObject updatePackage, String expectedBundleFileName,
242
+ DownloadProgressCallback progressCallback,
243
+ String downloadUrlString, boolean isBinaryPatchUpdate,
244
+ BinaryPatchAttempt patchAttempt) throws IOException {
148
245
  String newUpdateHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null);
149
246
  String newUpdateFolderPath = getPackageFolderPath(newUpdateHash);
150
247
  String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, CodePushConstants.PACKAGE_FILE_NAME);
@@ -154,7 +251,6 @@ public class CodePushUpdateManager {
154
251
  FileUtils.deleteDirectoryAtPath(newUpdateFolderPath);
155
252
  }
156
253
 
157
- String downloadUrlString = updatePackage.optString(CodePushConstants.DOWNLOAD_URL_KEY, null);
158
254
  HttpURLConnection connection = null;
159
255
  BufferedInputStream bin = null;
160
256
  FileOutputStream fos = null;
@@ -232,6 +328,21 @@ public class CodePushUpdateManager {
232
328
  FileUtils.unzipFile(downloadFile, unzippedFolderPath);
233
329
  FileUtils.deleteFileOrFolderSilently(downloadFile);
234
330
 
331
+ // Rebuild the JS bundle the archive only carries a patch of, which leaves the
332
+ // contents identical to the ones the full archive would have delivered.
333
+ if (isBinaryPatchUpdate) {
334
+ long patchStartTime = System.currentTimeMillis();
335
+ BinaryPatchResult patchResult = mBinaryPatch.restoreBundle(unzippedFolderPath,
336
+ getBinaryPatchFolderPath(), expectedBundleFileName);
337
+ if (!patchResult.succeeded()) {
338
+ return patchResult;
339
+ }
340
+
341
+ long applyDurationMs = System.currentTimeMillis() - patchStartTime;
342
+ patchAttempt.recordBundleRestored(applyDurationMs);
343
+ CodePushUtils.log("Restored the update from its binary patch in " + applyDurationMs + " ms.");
344
+ }
345
+
235
346
  // Merge contents with current update based on the manifest
236
347
  String diffManifestFilePath = CodePushUtils.appendPathComponent(unzippedFolderPath,
237
348
  CodePushConstants.DIFF_MANIFEST_FILE_NAME);
@@ -268,12 +379,22 @@ public class CodePushUpdateManager {
268
379
  CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath);
269
380
  }
270
381
  } else {
382
+ if (isBinaryPatchUpdate) {
383
+ // Whatever the patch URL served, it is not a patch archive - an error page
384
+ // answered with a 200 looks like this too. Moving it into place would
385
+ // install bytes no hash has ever been checked against, so the full archive
386
+ // is downloaded instead.
387
+ return BinaryPatchResult.failure(BinaryPatchResult.REASON_INVALID_MANIFEST);
388
+ }
389
+
271
390
  // File is a jsbundle, move it to a folder with the packageHash as its name
272
391
  FileUtils.moveFile(downloadFile, newUpdateFolderPath, expectedBundleFileName);
273
392
  }
274
393
 
275
394
  // Save metadata to the folder.
276
395
  CodePushUtils.writeJsonToFile(updatePackage, newUpdateMetadataPath);
396
+
397
+ return BinaryPatchResult.success();
277
398
  }
278
399
 
279
400
  public void installPackage(JSONObject updatePackage, boolean removePendingUpdate) {
@@ -349,4 +470,38 @@ public class CodePushUpdateManager {
349
470
  public void clearUpdates() {
350
471
  FileUtils.deleteDirectoryAtPath(getCodePushPath());
351
472
  }
473
+
474
+ /** Reads the JS bundle that shipped inside the app binary out of the APK's assets. */
475
+ private static class AssetsBaseBundleProvider implements CodePushBinaryPatch.BaseBundleProvider {
476
+
477
+ private final Context mContext;
478
+
479
+ AssetsBaseBundleProvider(Context context) {
480
+ mContext = context;
481
+ }
482
+
483
+ @Override
484
+ public byte[] readBaseBundle(String bundleFileName) throws IOException {
485
+ // The bundle is stored uncompressed in the APK, so it is read straight into
486
+ // memory: a copy on disk would buy nothing and cost the space the restored
487
+ // bundle needs.
488
+ InputStream assetStream = mContext.getAssets().open(bundleFileName);
489
+ try {
490
+ // An uncompressed asset knows its whole length up front, so the buffer is
491
+ // sized for it: growing one would repeatedly hold two copies of a bundle
492
+ // that is already the largest allocation on this path.
493
+ ByteArrayOutputStream bundleBytes = new ByteArrayOutputStream(
494
+ Math.max(assetStream.available(), CodePushConstants.DOWNLOAD_BUFFER_SIZE));
495
+ byte[] buffer = new byte[CodePushConstants.DOWNLOAD_BUFFER_SIZE];
496
+ int bytesRead;
497
+ while ((bytesRead = assetStream.read(buffer)) > 0) {
498
+ bundleBytes.write(buffer, 0, bytesRead);
499
+ }
500
+
501
+ return bundleBytes.toByteArray();
502
+ } finally {
503
+ assetStream.close();
504
+ }
505
+ }
506
+ }
352
507
  }