@bravemobile/react-native-code-push 13.0.0 → 13.1.0-beta.1
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.
- package/CodePush.podspec +35 -3
- package/README.md +117 -3
- package/THIRD-PARTY-NOTICES +96 -0
- package/android/app/build.gradle +19 -0
- package/android/app/proguard-rules.pro +5 -0
- package/android/app/src/main/cpp/CMakeLists.txt +38 -0
- package/android/app/src/main/cpp/binarypatch_jni.c +181 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +92 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchResult.java +64 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +1 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +293 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +25 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +20 -6
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +168 -4
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +13 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgressCallback.java +7 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgressEventDispatcher.java +25 -4
- package/android/app/src/main/java/com/microsoft/codepush/react/HDiffPatchNative.java +52 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +393 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +211 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +489 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/DownloadProgressEventDispatcherTest.java +80 -0
- package/android/codepush-export.gradle +115 -0
- package/cli/commands/bundleCommand/bundleCodePush.ts +39 -7
- package/cli/commands/bundleCommand/index.ts +9 -1
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
- package/cli/commands/releaseCommand/index.test.ts +118 -0
- package/cli/commands/releaseCommand/index.ts +20 -2
- package/cli/commands/releaseCommand/release.test.ts +582 -0
- package/cli/commands/releaseCommand/release.ts +232 -15
- package/cli/dist/commands/bundleCommand/bundleCodePush.js +26 -7
- package/cli/dist/commands/bundleCommand/index.js +5 -1
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
- package/cli/dist/commands/releaseCommand/index.js +8 -1
- package/cli/dist/commands/releaseCommand/index.test.js +96 -0
- package/cli/dist/commands/releaseCommand/release.js +165 -17
- package/cli/dist/commands/releaseCommand/release.test.js +367 -0
- package/cli/dist/functions/makeBinaryPatchBundle.js +248 -0
- package/cli/dist/functions/makeBinaryPatchBundle.test.js +321 -0
- package/cli/dist/functions/makeCodePushBundle.js +4 -2
- package/cli/dist/functions/runHermesEmitBinaryCommand.js +48 -1
- package/cli/dist/functions/runHermesEmitBinaryCommand.test.js +100 -0
- package/cli/dist/jest.globalSetup.js +38 -0
- package/cli/dist/utils/binaryPatch.js +86 -0
- package/cli/dist/utils/binaryPatch.test.js +190 -0
- package/cli/functions/makeBinaryPatchBundle.test.ts +410 -0
- package/cli/functions/makeBinaryPatchBundle.ts +367 -0
- package/cli/functions/makeCodePushBundle.ts +4 -2
- package/cli/functions/runHermesEmitBinaryCommand.test.ts +124 -0
- package/cli/functions/runHermesEmitBinaryCommand.ts +57 -1
- package/cli/jest.globalSetup.ts +43 -0
- package/cli/utils/binaryPatch.test.ts +234 -0
- package/cli/utils/binaryPatch.ts +103 -0
- package/cpp/binarypatch/binarypatch_zstd_decompressor.c +187 -0
- package/cpp/binarypatch/binarypatch_zstd_decompressor.h +47 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/checksum_plugin.h +52 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/hpatch_mt/hpatch_mt.h +147 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c +3315 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.h +268 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_private.h +297 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_types.h +434 -0
- package/cpp/binarypatch/vendor/zstd/common/allocations.h +55 -0
- package/cpp/binarypatch/vendor/zstd/common/bits.h +205 -0
- package/cpp/binarypatch/vendor/zstd/common/bitstream.h +454 -0
- package/cpp/binarypatch/vendor/zstd/common/compiler.h +464 -0
- package/cpp/binarypatch/vendor/zstd/common/cpu.h +249 -0
- package/cpp/binarypatch/vendor/zstd/common/debug.c +30 -0
- package/cpp/binarypatch/vendor/zstd/common/debug.h +107 -0
- package/cpp/binarypatch/vendor/zstd/common/entropy_common.c +340 -0
- package/cpp/binarypatch/vendor/zstd/common/error_private.c +64 -0
- package/cpp/binarypatch/vendor/zstd/common/error_private.h +158 -0
- package/cpp/binarypatch/vendor/zstd/common/fse.h +625 -0
- package/cpp/binarypatch/vendor/zstd/common/fse_decompress.c +315 -0
- package/cpp/binarypatch/vendor/zstd/common/huf.h +277 -0
- package/cpp/binarypatch/vendor/zstd/common/mem.h +422 -0
- package/cpp/binarypatch/vendor/zstd/common/pool.c +371 -0
- package/cpp/binarypatch/vendor/zstd/common/pool.h +81 -0
- package/cpp/binarypatch/vendor/zstd/common/portability_macros.h +171 -0
- package/cpp/binarypatch/vendor/zstd/common/threading.c +182 -0
- package/cpp/binarypatch/vendor/zstd/common/threading.h +142 -0
- package/cpp/binarypatch/vendor/zstd/common/xxhash.c +18 -0
- package/cpp/binarypatch/vendor/zstd/common/xxhash.h +7094 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_common.c +48 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_deps.h +123 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_internal.h +324 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_trace.h +156 -0
- package/cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c +1944 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c +244 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.h +44 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c +2410 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c +2209 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.h +73 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_internal.h +240 -0
- package/cpp/binarypatch/vendor/zstd/zstd.h +3206 -0
- package/cpp/binarypatch/vendor/zstd/zstd_errors.h +107 -0
- package/ios/CodePush/CodePush.h +18 -1
- package/ios/CodePush/CodePush.mm +24 -1
- package/ios/CodePush/CodePushBinaryPatch.h +80 -0
- package/ios/CodePush/CodePushBinaryPatch.m +473 -0
- package/ios/CodePush/CodePushDownloadHandler.m +6 -0
- package/ios/CodePush/CodePushPackage.m +183 -2
- package/ios/CodePush/CodePushUpdateUtils.m +49 -1
- package/package.json +10 -3
- package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
- package/scripts/export-embedded-bundle.sh +76 -0
- package/src/CodePush.js +46 -3
- package/src/CodePush.test.js +382 -0
- package/src/package-mixins.js +20 -2
- package/typings/react-native-code-push.d.ts +80 -1
|
@@ -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,9 +336,8 @@ 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
|
-
private volatile DownloadProgress latestDownloadProgress = null;
|
|
342
341
|
private final DownloadProgressEventDispatcher progressEventDispatcher = new DownloadProgressEventDispatcher(
|
|
343
342
|
new DownloadProgressEventDispatcher.EventEmitter() {
|
|
344
343
|
@Override
|
|
@@ -347,16 +346,21 @@ public class CodePushNativeModule extends NativeCodePushSpec {
|
|
|
347
346
|
}
|
|
348
347
|
});
|
|
349
348
|
|
|
349
|
+
@Override
|
|
350
|
+
public void onDownloadStart() {
|
|
351
|
+
progressEventDispatcher.reset();
|
|
352
|
+
}
|
|
353
|
+
|
|
350
354
|
@Override
|
|
351
355
|
public void call(DownloadProgress downloadProgress) {
|
|
352
356
|
if (!notifyProgress || downloadProgress == null) {
|
|
353
357
|
return;
|
|
354
358
|
}
|
|
355
359
|
|
|
356
|
-
|
|
360
|
+
progressEventDispatcher.record(downloadProgress);
|
|
357
361
|
// If the download is completed, synchronously send the last event.
|
|
358
362
|
if (downloadProgress.isCompleted()) {
|
|
359
|
-
progressEventDispatcher.
|
|
363
|
+
progressEventDispatcher.dispatchLatest();
|
|
360
364
|
return;
|
|
361
365
|
}
|
|
362
366
|
|
|
@@ -372,7 +376,7 @@ public class CodePushNativeModule extends NativeCodePushSpec {
|
|
|
372
376
|
@Override
|
|
373
377
|
public void doFrame(long frameTimeNanos) {
|
|
374
378
|
try {
|
|
375
|
-
progressEventDispatcher.
|
|
379
|
+
progressEventDispatcher.dispatchLatest();
|
|
376
380
|
} finally {
|
|
377
381
|
hasScheduledNextFrame = false;
|
|
378
382
|
}
|
|
@@ -384,7 +388,17 @@ public class CodePushNativeModule extends NativeCodePushSpec {
|
|
|
384
388
|
});
|
|
385
389
|
|
|
386
390
|
JSONObject newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY));
|
|
387
|
-
|
|
391
|
+
WritableMap downloadedPackage = CodePushUtils.convertJsonObjectToWritable(newPackage);
|
|
392
|
+
if (binaryPatchResult != null) {
|
|
393
|
+
// The result describes this download rather than the update, so it is
|
|
394
|
+
// put on what this call resolves with and nowhere else: the metadata
|
|
395
|
+
// the update manager has already written knows nothing about it, and
|
|
396
|
+
// whoever asked for the download decides what the result is worth.
|
|
397
|
+
downloadedPackage.putMap(CodePushConstants.BINARY_PATCH_RESULT_KEY,
|
|
398
|
+
CodePushUtils.convertJsonObjectToWritable(binaryPatchResult));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
promise.resolve(downloadedPackage);
|
|
388
402
|
} catch (CodePushInvalidUpdateException e) {
|
|
389
403
|
CodePushUtils.log(e);
|
|
390
404
|
mSettingsManager.saveFailedUpdate(CodePushUtils.convertReadableToJsonObject(updatePackage));
|